Skip to main content

sedsnet/
relay.rs

1use crate::config::{
2    RuntimeMemoryConfig, runtime_reliable_max_end_to_end_ack_cache,
3    runtime_reliable_max_end_to_end_pending, runtime_reliable_max_pending,
4    runtime_reliable_max_retries, runtime_reliable_max_return_routes,
5    runtime_reliable_retransmit_ms,
6};
7use crate::diagnostics::{
8    AdaptiveLinkStats, DiscoveryRuntimeStats, QueueRuntimeStats, ReliableRuntimeStats,
9    RouteModeStats, RouteOverrideStats, RoutePriorityStats, RouteWeightStats, RuntimeSideStats,
10    RuntimeStatsSnapshot, RuntimeTypeStats, TypedRouteOverrideStats,
11};
12#[cfg(feature = "discovery")]
13use crate::discovery::{
14    self, ClientStatsSnapshot, DISCOVERY_INCREMENTAL_RETRY_COUNT, DISCOVERY_ROUTE_TTL_MS,
15    DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS, DISCOVERY_SLOW_LINK_PING_INTERVAL_MS,
16    DiscoveryCadenceState, TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS, TopologyAnnouncerRoute,
17    TopologyBoardNode, TopologySideRoute, TopologySnapshot,
18};
19#[cfg(feature = "discovery")]
20use crate::packet::sender_address_u32;
21use crate::packet::{Packet, hash_bytes_u64};
22use crate::queue::{BoundedDeque, ByteCost};
23use crate::wire_format;
24use crate::{is_reliable_type, message_meta, message_priority, reliable_mode};
25use crate::{
26    router::{Clock, CompactTimestampOmissionPolicy, SideTransportProfile},
27    {
28        RouteSelectionMode, TelemetryError, TelemetryResult,
29        lock::{ReentryGate, ReentryGuard, RouterMutex},
30    },
31};
32use alloc::borrow::ToOwned;
33use alloc::boxed::Box;
34use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
35use alloc::string::{String, ToString};
36use alloc::{sync::Arc, vec, vec::Vec};
37use core::mem::size_of;
38
39const MAX_PRIORITY_BURST: u8 = 8;
40use crc32fast::Hasher as Crc32Hasher;
41
42/// Logical side index (CAN, UART, RADIO, etc.)
43pub type RelaySideId = usize;
44const SIDE_TRANSPORT_MAGIC: &[u8; 3] = b"SDT";
45const SIDE_TRANSPORT_KIND_FULL: u8 = 0x01;
46const SIDE_TRANSPORT_KIND_COMPACT: u8 = 0x02;
47const SIDE_TRANSPORT_KIND_CHUNK: u8 = 0x03;
48const SIDE_TRANSPORT_KIND_COMPACT_DELTA: u8 = 0x04;
49const SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP: u8 = 0x05;
50const SIDE_TRANSPORT_FLAG_PAYLOAD_COMPRESSED: u8 = 0x01;
51const SIDE_TRANSPORT_FLAG_WIRE_CONTRACT: u8 = 0x04;
52const SIDE_TRANSPORT_FLAG_PACKET_NONCE: u8 = 0x08;
53const SIDE_TRANSPORT_FLAG_ENDPOINT_BITMAP_PRESENT: u8 = 0x20;
54const SIDE_TRANSPORT_FLAG_COMPACT_RELIABLE_HEADER: u8 = 0x40;
55const CONTROL_SLOW_LINK_CAPACITY_BPS: u64 = 512;
56const SIDE_TRANSPORT_CHUNK_OVERHEAD: usize = 3 + 1 + 4 + 2 + 2 + wire_format::CRC32_BYTES;
57const SIDE_TRANSPORT_EP_BITMAP_BITS: usize = (crate::MAX_VALUE_DATA_ENDPOINT as usize) + 1;
58const SIDE_TRANSPORT_EP_BITMAP_BYTES: usize = SIDE_TRANSPORT_EP_BITMAP_BITS.div_ceil(8);
59pub const IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES: usize =
60    crate::router::IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES;
61pub const IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES: usize =
62    crate::router::IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
63pub const DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT: usize =
64    crate::router::DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT;
65/// Packet Handler function type
66type PacketHandlerFn = dyn Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static;
67
68/// Packed Handler function type
69type PackedHandlerFn = dyn Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static;
70
71/// TX handler for a relay side: either packed or packet-based.
72#[derive(Clone)]
73pub enum RelayTxHandlerFn {
74    Packed(Arc<PackedHandlerFn>),
75    Packet(Arc<PacketHandlerFn>),
76}
77
78#[derive(Clone, Copy, Debug)]
79pub struct RelaySideOptions {
80    /// Enables the relay's per-link reliable transport layer on this side.
81    ///
82    /// When `true` and the side uses a packed TX handler, reliable schema traffic on this hop
83    /// gains relay-managed sequence numbers, ACKs, packet requests, and retransmits.
84    /// Packet-output sides still receive decoded packets rather than packed reliable framing.
85    pub reliable_enabled: bool,
86    /// Marks the side as eligible for link-local-only endpoints and discovery routes.
87    pub link_local_enabled: bool,
88    /// Allows packets received from this side to enter relay processing.
89    pub ingress_enabled: bool,
90    /// Allows the relay to transmit packets toward this side.
91    pub egress_enabled: bool,
92    /// Enables side-local header-template reuse for packed transport.
93    pub header_template_enabled: bool,
94    /// Maximum number of bytes to emit per packed TX callback.
95    ///
96    /// When non-zero and a packed frame would exceed this size, the relay
97    /// splits it into ordered side-transport chunks and reassembles them on RX
98    /// before normal relay processing. This is intended for fixed-size links
99    /// such as CAN or I2C while keeping the user API packet-oriented.
100    pub max_frame_bytes: usize,
101    /// Target total side-transport overhead for compact follow-up frames.
102    pub compact_header_target_bytes: usize,
103    /// Maximum side-local header templates retained for TX and RX dictionaries.
104    pub max_side_transport_templates: usize,
105    /// Omits the timestamp field from compact follow-up frames when it is unchanged.
106    pub omit_unchanged_compact_timestamps: bool,
107    /// Optional per-data-type timestamp omission policy for compact follow-up frames.
108    pub compact_timestamp_omission_types: CompactTimestampOmissionPolicy,
109    /// Declared compact-link profile for stats and future negotiation.
110    pub side_transport_profile: SideTransportProfile,
111}
112
113impl Default for RelaySideOptions {
114    fn default() -> Self {
115        Self {
116            reliable_enabled: false,
117            link_local_enabled: false,
118            ingress_enabled: true,
119            egress_enabled: true,
120            header_template_enabled: false,
121            max_frame_bytes: 0,
122            compact_header_target_bytes: 0,
123            max_side_transport_templates: DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT,
124            omit_unchanged_compact_timestamps: false,
125            compact_timestamp_omission_types: CompactTimestampOmissionPolicy::none(),
126            side_transport_profile: SideTransportProfile::Canonical,
127        }
128    }
129}
130
131impl RelaySideOptions {
132    /// Convenience preset for bounded packed-side transport.
133    ///
134    /// `max_frame_bytes == 0` leaves packed frames unbounded. Values greater
135    /// than zero enable relay-managed chunking/reassembly on this side.
136    #[inline]
137    pub fn with_small_packet_transport(mut self, max_frame_bytes: usize) -> Self {
138        self.header_template_enabled = true;
139        self.max_frame_bytes = max_frame_bytes;
140        self.compact_header_target_bytes = IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
141        self.side_transport_profile = SideTransportProfile::Ipv6Like;
142        self
143    }
144
145    #[inline]
146    pub fn with_ipv4_like_compact_header_target(mut self) -> Self {
147        self.header_template_enabled = true;
148        self.compact_header_target_bytes = IPV4_LIKE_COMPACT_HEADER_TARGET_BYTES;
149        self.omit_unchanged_compact_timestamps = true;
150        self.side_transport_profile = SideTransportProfile::Ipv4Like;
151        self
152    }
153
154    #[inline]
155    pub fn with_ipv6_like_compact_header_target(mut self) -> Self {
156        self.header_template_enabled = true;
157        self.compact_header_target_bytes = IPV6_LIKE_COMPACT_HEADER_TARGET_BYTES;
158        self.side_transport_profile = SideTransportProfile::Ipv6Like;
159        self
160    }
161
162    #[inline]
163    pub fn with_template_transport(mut self) -> Self {
164        self.header_template_enabled = true;
165        self.side_transport_profile = SideTransportProfile::Template;
166        self
167    }
168
169    #[inline]
170    pub fn with_omitted_unchanged_compact_timestamps(mut self) -> Self {
171        self.header_template_enabled = true;
172        self.omit_unchanged_compact_timestamps = true;
173        self
174    }
175
176    #[inline]
177    pub fn with_omitted_unchanged_compact_timestamps_for_type(
178        mut self,
179        ty: crate::DataType,
180    ) -> Self {
181        self.header_template_enabled = true;
182        self.compact_timestamp_omission_types.insert(ty);
183        self
184    }
185
186    #[inline]
187    pub fn effective_transport_profile(self) -> SideTransportProfile {
188        if !self.header_template_enabled && self.max_frame_bytes == 0 {
189            SideTransportProfile::Canonical
190        } else if self.side_transport_profile == SideTransportProfile::Canonical {
191            SideTransportProfile::Template
192        } else {
193            self.side_transport_profile
194        }
195    }
196
197    #[cfg(feature = "discovery")]
198    #[inline]
199    pub fn link_capabilities(self) -> discovery::LinkCapabilities {
200        let mut flags = discovery::LINK_CAPABILITY_END_TO_END_RELIABILITY;
201        if self.header_template_enabled {
202            flags |= discovery::LINK_CAPABILITY_HEADER_TEMPLATES;
203        }
204        if self.max_frame_bytes != 0 {
205            flags |= discovery::LINK_CAPABILITY_CHUNKING;
206        }
207        if self.reliable_enabled {
208            flags |= discovery::LINK_CAPABILITY_RELIABILITY;
209        }
210        if self.omit_unchanged_compact_timestamps
211            || !self.compact_timestamp_omission_types.is_empty()
212        {
213            flags |= discovery::LINK_CAPABILITY_OMIT_UNCHANGED_TIMESTAMPS;
214        }
215        #[cfg(feature = "cryptography")]
216        {
217            flags |= discovery::LINK_CAPABILITY_CRYPTO;
218        }
219        discovery::LinkCapabilities {
220            version: 1,
221            flags,
222            profile: self.effective_transport_profile().discovery_code(),
223            max_frame_bytes: self.max_frame_bytes.min(u32::MAX as usize) as u32,
224            compact_header_target_bytes: self.compact_header_target_bytes.min(u32::MAX as usize)
225                as u32,
226            max_side_transport_templates: self.max_side_transport_templates.min(u32::MAX as usize)
227                as u32,
228        }
229    }
230}
231
232/// One side of the relay – a name + TX handler.
233#[derive(Clone)]
234pub struct RelaySide {
235    pub name: Arc<str>,
236    pub tx_handler: RelayTxHandlerFn,
237    pub opts: RelaySideOptions,
238}
239
240#[derive(Clone, Debug, PartialEq, Eq)]
241pub enum RelayItem {
242    Packed(Arc<[u8]>),
243    Packet(Arc<Packet>),
244}
245
246/// Item that was received by the relay from some side.
247#[derive(Clone, Debug, PartialEq, Eq)]
248struct RelayRxItem {
249    src: RelaySideId,
250    data: RelayItem,
251    priority: u8,
252}
253
254impl ByteCost for RelayRxItem {
255    fn byte_cost(&self) -> usize {
256        match &self.data {
257            RelayItem::Packed(bytes) => bytes.len(),
258            RelayItem::Packet(pkt) => pkt.byte_cost(),
259        }
260    }
261}
262
263/// Item that is ready to be transmitted out a destination side.
264#[derive(Clone, Debug, PartialEq, Eq)]
265struct RelayTxItem {
266    src: Option<RelaySideId>,
267    dst: RelaySideId,
268    data: RelayItem,
269    priority: u8,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq)]
273struct RelayReplayItem {
274    dst: RelaySideId,
275    bytes: Arc<[u8]>,
276    priority: u8,
277}
278
279impl ByteCost for RelayTxItem {
280    fn byte_cost(&self) -> usize {
281        match &self.data {
282            RelayItem::Packed(bytes) => bytes.len(),
283            RelayItem::Packet(pkt) => pkt.byte_cost(),
284        }
285    }
286}
287
288impl ByteCost for RelayReplayItem {
289    fn byte_cost(&self) -> usize {
290        self.bytes.len()
291    }
292}
293
294// -------------------- Reliable delivery state (relay) --------------------
295
296#[derive(Debug, Clone)]
297struct ReliableTxState {
298    next_seq: u32,
299    sent_order: VecDeque<u32>,
300    sent: BTreeMap<u32, ReliableSent>,
301}
302
303#[derive(Debug, Clone)]
304struct ReliableSent {
305    bytes: Arc<[u8]>,
306    last_send_ms: u64,
307    retries: u32,
308    queued: bool,
309    partial_acked: bool,
310}
311
312#[derive(Debug, Clone)]
313struct ReliableRxState {
314    expected_seq: u32,
315    buffered: BTreeMap<u32, Arc<[u8]>>,
316}
317
318#[derive(Debug, Clone)]
319struct ReliableReturnRouteState {
320    side: RelaySideId,
321}
322
323#[cfg(feature = "discovery")]
324#[derive(Debug, Clone, Default, PartialEq, Eq)]
325struct DiscoverySenderState {
326    /// Numeric identity advertised for this hostname. Compact frames may carry
327    /// this address instead of the hostname, so routing must accept either.
328    advertised_address: Option<u32>,
329    reachable: Vec<crate::DataEndpoint>,
330    advertised_reachable: Vec<crate::DataEndpoint>,
331    reachable_timesync_sources: Vec<String>,
332    advertised_reachable_timesync_sources: Vec<String>,
333    topology_boards: Vec<TopologyBoardNode>,
334    last_seen_ms: u64,
335}
336
337#[inline]
338fn is_internal_control_type(ty: crate::DataType) -> bool {
339    if matches!(
340        ty,
341        crate::DataType::ReliableAck
342            | crate::DataType::ReliablePartialAck
343            | crate::DataType::ReliablePacketRequest
344            | crate::DataType::P2pMessage
345    ) {
346        return true;
347    }
348
349    #[cfg(feature = "timesync")]
350    if matches!(
351        ty,
352        crate::DataType::TimeSyncAnnounce
353            | crate::DataType::TimeSyncRequest
354            | crate::DataType::TimeSyncResponse
355    ) {
356        return true;
357    }
358
359    #[cfg(feature = "discovery")]
360    if discovery::is_discovery_type(ty) {
361        return true;
362    }
363
364    let _ = ty;
365    false
366}
367
368#[cfg(feature = "discovery")]
369#[derive(Debug, Clone, Default, PartialEq, Eq)]
370struct DiscoverySideState {
371    reachable: Vec<crate::DataEndpoint>,
372    reachable_timesync_sources: Vec<String>,
373    last_seen_ms: u64,
374    announcers: BTreeMap<String, DiscoverySenderState>,
375}
376
377#[derive(Clone, Debug, Default, PartialEq, Eq)]
378struct SideChunkAssembly {
379    total: u16,
380    received: BTreeMap<u16, Arc<[u8]>>,
381}
382
383#[derive(Clone, Debug, Default)]
384struct SideTransportState {
385    tx_template_ids: BTreeMap<u64, u32>,
386    tx_templates: BTreeMap<u64, SideHeaderTemplate>,
387    tx_last_timestamps: BTreeMap<u32, u64>,
388    tx_compact_uses: BTreeMap<u32, u8>,
389    rx_templates: BTreeMap<u64, SideHeaderTemplate>,
390    rx_templates_by_id: BTreeMap<u32, SideHeaderTemplate>,
391    rx_last_timestamps: BTreeMap<u32, u64>,
392    rx_chunks: BTreeMap<u32, SideChunkAssembly>,
393    next_chunk_id: u32,
394    next_template_id: u32,
395}
396
397impl SideTransportState {
398    fn clear_header_templates(&mut self) {
399        self.tx_template_ids.clear();
400        self.tx_templates.clear();
401        self.tx_last_timestamps.clear();
402        self.tx_compact_uses.clear();
403        self.rx_templates.clear();
404        self.rx_templates_by_id.clear();
405        self.rx_last_timestamps.clear();
406    }
407
408    fn tx_template_count(&self) -> usize {
409        self.tx_template_ids.len()
410    }
411
412    fn rx_template_count(&self) -> usize {
413        self.rx_templates_by_id.len()
414    }
415
416    fn insert_tx_template(
417        &mut self,
418        template: SideHeaderTemplate,
419        template_id: u32,
420        max_templates: usize,
421    ) -> bool {
422        if max_templates == 0 {
423            return false;
424        }
425        let mut evicted = false;
426        if self.tx_template_ids.len() >= max_templates
427            && !self.tx_template_ids.contains_key(&template.hash)
428            && let Some(old_hash) = self.tx_template_ids.keys().next().copied()
429        {
430            if let Some(old_id) = self.tx_template_ids.remove(&old_hash) {
431                self.tx_last_timestamps.remove(&old_id);
432                self.tx_compact_uses.remove(&old_id);
433            }
434            self.tx_templates.remove(&old_hash);
435            evicted = true;
436        }
437        self.tx_template_ids.insert(template.hash, template_id);
438        self.tx_templates.insert(template.hash, template);
439        self.tx_compact_uses.insert(template_id, 0);
440        evicted
441    }
442
443    fn insert_rx_template(
444        &mut self,
445        template_id: u32,
446        template: SideHeaderTemplate,
447        max_templates: usize,
448    ) -> bool {
449        if max_templates == 0 {
450            return false;
451        }
452        let mut evicted = false;
453        if self.rx_templates_by_id.len() >= max_templates
454            && !self.rx_templates_by_id.contains_key(&template_id)
455            && let Some(old_id) = self.rx_templates_by_id.keys().next().copied()
456            && let Some(old_template) = self.rx_templates_by_id.remove(&old_id)
457        {
458            self.rx_templates.remove(&old_template.hash);
459            self.rx_last_timestamps.remove(&old_id);
460            evicted = true;
461        }
462        self.rx_templates_by_id
463            .insert(template_id, template.clone());
464        self.rx_templates.insert(template.hash, template);
465        evicted
466    }
467}
468
469#[derive(Clone, Debug, PartialEq, Eq)]
470struct SideHeaderTemplate {
471    hash: u64,
472    base_flags: u8,
473    prefix: Arc<[u8]>,
474    between: Arc<[u8]>,
475    reliable_flags: Option<u8>,
476    reliable_compact: bool,
477}
478
479type SideTemplateExtract<'a> = (
480    SideHeaderTemplate,
481    crate::DataType,
482    u8,
483    u64,
484    u64,
485    u16,
486    Option<(u32, u32)>,
487    &'a [u8],
488);
489
490#[derive(Clone, Copy, Debug, PartialEq, Eq)]
491enum SideCompactTimestampMode {
492    Absolute,
493    Delta,
494    Omitted,
495}
496
497#[derive(Debug, Clone, Default)]
498struct AdaptiveRouteStats {
499    estimated_bandwidth_bps: u64,
500    peak_bandwidth_bps: u64,
501    last_observed_ms: u64,
502    last_slow_observed_ms: u64,
503    sample_count: u64,
504    window_started_ms: u64,
505    window_bytes: u64,
506    peak_usage_bps: u64,
507}
508
509#[cfg(feature = "discovery")]
510#[derive(Debug, Clone, Default)]
511struct DiscoverySideThrottleState {
512    next_ping_ms: u64,
513    next_full_ms: u64,
514    pending_incremental: bool,
515    incremental_retries_remaining: u8,
516    has_sent_full: bool,
517    last_topology: Vec<TopologyBoardNode>,
518}
519
520#[cfg(all(feature = "discovery", feature = "timesync"))]
521#[derive(Debug, Clone, Default)]
522struct TimeSyncSideThrottleState {
523    next_allowed_ms: u64,
524}
525
526#[cfg(feature = "discovery")]
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528enum DiscoveryAdvertiseLevel {
529    MinimalPing,
530    Incremental,
531    Full,
532}
533
534impl AdaptiveRouteStats {
535    #[inline]
536    fn observe(&mut self, bytes: usize, sample_bps: u64, now_ms: u64) {
537        self.estimated_bandwidth_bps = if self.estimated_bandwidth_bps == 0 {
538            sample_bps
539        } else if sample_bps >= self.estimated_bandwidth_bps {
540            self.estimated_bandwidth_bps
541                .saturating_mul(3)
542                .saturating_add(sample_bps.saturating_mul(5))
543                / 8
544        } else {
545            self.estimated_bandwidth_bps
546                .saturating_mul(7)
547                .saturating_add(sample_bps)
548                / 8
549        };
550        self.peak_bandwidth_bps = self.peak_bandwidth_bps.max(sample_bps);
551        self.last_observed_ms = now_ms;
552        if sample_bps > 0 && sample_bps <= CONTROL_SLOW_LINK_CAPACITY_BPS {
553            self.last_slow_observed_ms = now_ms;
554        }
555        self.sample_count = self.sample_count.saturating_add(1);
556        if self.window_started_ms == 0 || now_ms.saturating_sub(self.window_started_ms) > 1_000 {
557            self.window_started_ms = now_ms;
558            self.window_bytes = 0;
559        }
560        self.window_bytes = self.window_bytes.saturating_add(bytes as u64);
561        self.peak_usage_bps = self.peak_usage_bps.max(self.current_usage_bps(now_ms));
562    }
563
564    #[inline]
565    fn current_usage_bps(&self, now_ms: u64) -> u64 {
566        if self.window_started_ms == 0 {
567            return 0;
568        }
569        let elapsed_ms = now_ms.saturating_sub(self.window_started_ms).max(1);
570        (u128::from(self.window_bytes).saturating_mul(1000) / u128::from(elapsed_ms))
571            .min(u128::from(u64::MAX)) as u64
572    }
573
574    #[inline]
575    fn available_headroom_bps(&self, now_ms: u64) -> u64 {
576        let capacity = self
577            .estimated_bandwidth_bps
578            .max(self.peak_bandwidth_bps)
579            .max(1);
580        capacity.saturating_sub(self.current_usage_bps(now_ms))
581    }
582
583    #[inline]
584    fn weight(&self, now_ms: u64) -> u64 {
585        self.available_headroom_bps(now_ms).max(1)
586    }
587
588    #[inline]
589    fn snapshot(&self, now_ms: u64, auto_balancing_enabled: bool) -> AdaptiveLinkStats {
590        let current_usage_bps = self.current_usage_bps(now_ms);
591        let estimated_capacity_bps = self.estimated_bandwidth_bps.max(1);
592        let peak_capacity_bps = self.peak_bandwidth_bps.max(estimated_capacity_bps);
593        let available_headroom_bps = peak_capacity_bps.saturating_sub(current_usage_bps);
594        AdaptiveLinkStats {
595            auto_balancing_enabled,
596            estimated_capacity_bps,
597            peak_capacity_bps,
598            current_usage_bps,
599            peak_usage_bps: self.peak_usage_bps.max(current_usage_bps),
600            available_headroom_bps,
601            effective_weight: available_headroom_bps.max(1),
602            last_observed_ms: self.last_observed_ms,
603            sample_count: self.sample_count,
604        }
605    }
606}
607
608#[derive(Debug, Clone, Default)]
609struct TypeRuntimeStatsInner {
610    tx_packets: u64,
611    tx_bytes: u64,
612    rx_packets: u64,
613    rx_bytes: u64,
614    relayed_tx_packets: u64,
615    relayed_tx_bytes: u64,
616    relayed_rx_packets: u64,
617    relayed_rx_bytes: u64,
618    tx_retries: u64,
619    handler_failures: u64,
620}
621
622#[derive(Debug, Clone, Default)]
623struct SideRuntimeStatsInner {
624    tx_packets: u64,
625    tx_bytes: u64,
626    rx_packets: u64,
627    rx_bytes: u64,
628    relayed_tx_packets: u64,
629    relayed_tx_bytes: u64,
630    relayed_rx_packets: u64,
631    relayed_rx_bytes: u64,
632    tx_retries: u64,
633    tx_handler_failures: u64,
634    total_handler_retries: u64,
635    side_transport_full_frames: u64,
636    side_transport_compact_frames: u64,
637    side_transport_compact_delta_frames: u64,
638    side_transport_compact_omitted_timestamp_frames: u64,
639    side_transport_chunk_frames: u64,
640    side_transport_raw_bytes: u64,
641    side_transport_wire_bytes: u64,
642    side_transport_bytes_saved: u64,
643    side_transport_min_compact_overhead_bytes: Option<usize>,
644    side_transport_max_compact_overhead_bytes: Option<usize>,
645    side_transport_compact_target_misses: u64,
646    side_transport_template_evictions: u64,
647    data_types: BTreeMap<u32, TypeRuntimeStatsInner>,
648}
649
650impl SideRuntimeStatsInner {
651    fn type_stats_mut(&mut self, ty: crate::DataType) -> &mut TypeRuntimeStatsInner {
652        self.data_types.entry(ty.as_u32()).or_default()
653    }
654
655    fn note_tx(&mut self, ty: crate::DataType, bytes: usize, retries: usize) {
656        self.tx_packets = self.tx_packets.saturating_add(1);
657        self.tx_bytes = self.tx_bytes.saturating_add(bytes as u64);
658        self.relayed_tx_packets = self.relayed_tx_packets.saturating_add(1);
659        self.relayed_tx_bytes = self.relayed_tx_bytes.saturating_add(bytes as u64);
660        self.tx_retries = self.tx_retries.saturating_add(retries as u64);
661        self.total_handler_retries = self.total_handler_retries.saturating_add(retries as u64);
662        let stats = self.type_stats_mut(ty);
663        stats.tx_packets = stats.tx_packets.saturating_add(1);
664        stats.tx_bytes = stats.tx_bytes.saturating_add(bytes as u64);
665        stats.relayed_tx_packets = stats.relayed_tx_packets.saturating_add(1);
666        stats.relayed_tx_bytes = stats.relayed_tx_bytes.saturating_add(bytes as u64);
667        stats.tx_retries = stats.tx_retries.saturating_add(retries as u64);
668    }
669
670    fn note_rx(&mut self, ty: crate::DataType, bytes: usize) {
671        self.rx_packets = self.rx_packets.saturating_add(1);
672        self.rx_bytes = self.rx_bytes.saturating_add(bytes as u64);
673        self.relayed_rx_packets = self.relayed_rx_packets.saturating_add(1);
674        self.relayed_rx_bytes = self.relayed_rx_bytes.saturating_add(bytes as u64);
675        let stats = self.type_stats_mut(ty);
676        stats.rx_packets = stats.rx_packets.saturating_add(1);
677        stats.rx_bytes = stats.rx_bytes.saturating_add(bytes as u64);
678        stats.relayed_rx_packets = stats.relayed_rx_packets.saturating_add(1);
679        stats.relayed_rx_bytes = stats.relayed_rx_bytes.saturating_add(bytes as u64);
680    }
681
682    fn note_tx_failure(&mut self, ty: crate::DataType, retries: usize) {
683        self.tx_handler_failures = self.tx_handler_failures.saturating_add(1);
684        self.tx_retries = self.tx_retries.saturating_add(retries as u64);
685        self.total_handler_retries = self.total_handler_retries.saturating_add(retries as u64);
686        let stats = self.type_stats_mut(ty);
687        stats.handler_failures = stats.handler_failures.saturating_add(1);
688        stats.tx_retries = stats.tx_retries.saturating_add(retries as u64);
689    }
690
691    fn note_side_transport_full(&mut self, raw_bytes: usize, wire_bytes: usize) {
692        self.side_transport_full_frames = self.side_transport_full_frames.saturating_add(1);
693        self.note_side_transport_bytes(raw_bytes, wire_bytes);
694    }
695
696    fn note_side_transport_compact(
697        &mut self,
698        raw_bytes: usize,
699        wire_bytes: usize,
700        compact_overhead_bytes: usize,
701        used_timestamp_delta: bool,
702        omitted_timestamp: bool,
703    ) {
704        self.side_transport_compact_frames = self.side_transport_compact_frames.saturating_add(1);
705        if used_timestamp_delta {
706            self.side_transport_compact_delta_frames =
707                self.side_transport_compact_delta_frames.saturating_add(1);
708        }
709        if omitted_timestamp {
710            self.side_transport_compact_omitted_timestamp_frames = self
711                .side_transport_compact_omitted_timestamp_frames
712                .saturating_add(1);
713        }
714        self.note_side_transport_bytes(raw_bytes, wire_bytes);
715        self.side_transport_min_compact_overhead_bytes = Some(
716            self.side_transport_min_compact_overhead_bytes
717                .map_or(compact_overhead_bytes, |v| v.min(compact_overhead_bytes)),
718        );
719        self.side_transport_max_compact_overhead_bytes = Some(
720            self.side_transport_max_compact_overhead_bytes
721                .map_or(compact_overhead_bytes, |v| v.max(compact_overhead_bytes)),
722        );
723    }
724
725    fn note_side_transport_chunks(&mut self, chunks: usize) {
726        self.side_transport_chunk_frames = self
727            .side_transport_chunk_frames
728            .saturating_add(chunks as u64);
729    }
730
731    fn note_side_transport_bytes(&mut self, raw_bytes: usize, wire_bytes: usize) {
732        self.side_transport_raw_bytes = self
733            .side_transport_raw_bytes
734            .saturating_add(raw_bytes as u64);
735        self.side_transport_wire_bytes = self
736            .side_transport_wire_bytes
737            .saturating_add(wire_bytes as u64);
738        if raw_bytes > wire_bytes {
739            self.side_transport_bytes_saved = self
740                .side_transport_bytes_saved
741                .saturating_add((raw_bytes - wire_bytes) as u64);
742        }
743    }
744
745    fn note_side_transport_compact_target_miss(&mut self) {
746        self.side_transport_compact_target_misses =
747            self.side_transport_compact_target_misses.saturating_add(1);
748    }
749
750    fn note_side_transport_template_eviction(&mut self) {
751        self.side_transport_template_evictions =
752            self.side_transport_template_evictions.saturating_add(1);
753    }
754}
755
756#[derive(Clone, Debug)]
757pub struct RelayConfig {
758    sender: Arc<str>,
759    memory: RuntimeMemoryConfig,
760}
761
762impl RelayConfig {
763    pub fn new() -> Self {
764        Self::default()
765    }
766
767    pub fn with_sender<S: AsRef<str>>(mut self, sender: S) -> Self {
768        self.sender = Arc::from(sender.as_ref());
769        self
770    }
771
772    pub fn with_memory_config(mut self, memory: RuntimeMemoryConfig) -> TelemetryResult<Self> {
773        memory.validate()?;
774        self.memory = memory;
775        Ok(self)
776    }
777
778    fn sender(&self) -> Arc<str> {
779        self.sender.clone()
780    }
781
782    fn memory_config(&self) -> RuntimeMemoryConfig {
783        self.memory
784    }
785}
786
787impl Default for RelayConfig {
788    fn default() -> Self {
789        Self {
790            sender: Arc::from("RELAY"),
791            memory: RuntimeMemoryConfig::default(),
792        }
793    }
794}
795
796#[derive(Clone, Copy, Debug, PartialEq, Eq)]
797enum RouteSelectionOrigin {
798    Flood,
799    Discovered,
800}
801
802/// Internal state, protected by RouterMutex so all public methods can take &self.
803struct RelayInner {
804    memory: RuntimeMemoryConfig,
805    sides: Vec<Option<RelaySide>>,
806    route_overrides: BTreeMap<(Option<RelaySideId>, RelaySideId), bool>,
807    typed_route_overrides: BTreeMap<(Option<RelaySideId>, u32, RelaySideId), bool>,
808    route_weights: BTreeMap<(Option<RelaySideId>, RelaySideId), u32>,
809    route_priorities: BTreeMap<(Option<RelaySideId>, RelaySideId), u32>,
810    source_route_modes: BTreeMap<Option<RelaySideId>, RouteSelectionMode>,
811    route_selection_cursors: BTreeMap<Option<RelaySideId>, u64>,
812    adaptive_route_stats: BTreeMap<RelaySideId, AdaptiveRouteStats>,
813    side_runtime_stats: BTreeMap<RelaySideId, SideRuntimeStatsInner>,
814    side_transport: BTreeMap<RelaySideId, SideTransportState>,
815    rx_queue: BoundedDeque<RelayRxItem>,
816    tx_queue: BoundedDeque<RelayTxItem>,
817    tx_priority_burst: u8,
818    replay_queue: BoundedDeque<RelayReplayItem>,
819    recent_rx: BoundedDeque<u64>,
820    reliable_tx: BTreeMap<(RelaySideId, u32), ReliableTxState>,
821    reliable_rx: BTreeMap<(RelaySideId, u32), ReliableRxState>,
822    reliable_return_routes: BTreeMap<u64, ReliableReturnRouteState>,
823    reliable_return_route_order: VecDeque<u64>,
824    end_to_end_acked_destinations: BTreeMap<u64, BTreeSet<u64>>,
825    end_to_end_acked_destination_order: VecDeque<u64>,
826    total_handler_failures: u64,
827    total_handler_retries: u64,
828    #[cfg(feature = "discovery")]
829    discovery_routes: BTreeMap<RelaySideId, DiscoverySideState>,
830    #[cfg(feature = "discovery")]
831    discovery_cadence: DiscoveryCadenceState,
832    #[cfg(feature = "discovery")]
833    discovery_side_throttle: BTreeMap<RelaySideId, DiscoverySideThrottleState>,
834    #[cfg(all(feature = "discovery", feature = "timesync"))]
835    timesync_side_throttle: BTreeMap<RelaySideId, TimeSyncSideThrottleState>,
836}
837
838#[derive(Clone, Copy, Debug, PartialEq, Eq)]
839enum RelayQueueKind {
840    Rx,
841    Tx,
842    Replay,
843    Recent,
844    ReliableRxBuffer,
845    #[cfg(feature = "discovery")]
846    Discovery,
847}
848
849impl RelayInner {
850    #[cfg(feature = "discovery")]
851    fn topology_board_byte_cost(board: &TopologyBoardNode) -> usize {
852        board
853            .sender_id
854            .len()
855            .saturating_add(board.reachable_endpoints.len() * size_of::<crate::DataEndpoint>())
856            .saturating_add(
857                board
858                    .reachable_timesync_sources
859                    .iter()
860                    .map(|s| s.len())
861                    .sum::<usize>(),
862            )
863            .saturating_add(board.connections.iter().map(|s| s.len()).sum::<usize>())
864    }
865
866    #[cfg(feature = "discovery")]
867    fn discovery_sender_byte_cost(sender: &str, state: &DiscoverySenderState) -> usize {
868        sender
869            .len()
870            .saturating_add(state.reachable.len() * size_of::<crate::DataEndpoint>())
871            .saturating_add(state.advertised_reachable.len() * size_of::<crate::DataEndpoint>())
872            .saturating_add(
873                state
874                    .reachable_timesync_sources
875                    .iter()
876                    .map(|s| s.len())
877                    .sum::<usize>(),
878            )
879            .saturating_add(
880                state
881                    .advertised_reachable_timesync_sources
882                    .iter()
883                    .map(|s| s.len())
884                    .sum::<usize>(),
885            )
886            .saturating_add(
887                state
888                    .topology_boards
889                    .iter()
890                    .map(Self::topology_board_byte_cost)
891                    .sum::<usize>(),
892            )
893            .saturating_add(size_of::<DiscoverySenderState>())
894    }
895
896    #[cfg(feature = "discovery")]
897    fn discovery_route_byte_cost(route: &DiscoverySideState) -> usize {
898        size_of::<DiscoverySideState>()
899            .saturating_add(route.reachable.len() * size_of::<crate::DataEndpoint>())
900            .saturating_add(
901                route
902                    .reachable_timesync_sources
903                    .iter()
904                    .map(|s| s.len())
905                    .sum::<usize>(),
906            )
907            .saturating_add(
908                route
909                    .announcers
910                    .iter()
911                    .map(|(sender, state)| Self::discovery_sender_byte_cost(sender, state))
912                    .sum::<usize>(),
913            )
914    }
915
916    #[cfg(feature = "discovery")]
917    fn discovery_bytes_used(&self) -> usize {
918        self.discovery_routes
919            .values()
920            .map(Self::discovery_route_byte_cost)
921            .sum()
922    }
923
924    #[inline]
925    fn reliable_rx_buffered_bytes(&self) -> usize {
926        self.reliable_rx
927            .values()
928            .flat_map(|state| state.buffered.values())
929            .map(|bytes| size_of::<Arc<[u8]>>() + bytes.len())
930            .sum()
931    }
932
933    #[inline]
934    fn shared_queue_bytes_used(&self) -> usize {
935        self.rx_queue
936            .bytes_used()
937            .saturating_add(self.tx_queue.bytes_used())
938            .saturating_add(self.replay_queue.bytes_used())
939            .saturating_add(self.recent_rx.max_bytes())
940            .saturating_add(self.reliable_rx_buffered_bytes())
941            .saturating_add(crate::config::schema_bytes_used())
942            .saturating_add({
943                #[cfg(feature = "discovery")]
944                {
945                    self.discovery_bytes_used()
946                }
947                #[cfg(not(feature = "discovery"))]
948                {
949                    0
950                }
951            })
952    }
953
954    fn reliable_rx_buffer_len(&self) -> usize {
955        self.reliable_rx
956            .values()
957            .map(|state| state.buffered.len())
958            .sum()
959    }
960
961    fn pop_reliable_rx_buffered(&mut self) -> Option<Arc<[u8]>> {
962        let key = self
963            .reliable_rx
964            .iter()
965            .find_map(|(key, state)| (!state.buffered.is_empty()).then_some(*key))?;
966        self.reliable_rx
967            .get_mut(&key)?
968            .buffered
969            .pop_first()
970            .map(|(_, v)| v)
971    }
972
973    fn pop_shared_queue_item(&mut self, preferred: RelayQueueKind) -> bool {
974        match preferred {
975            RelayQueueKind::Rx => self.rx_queue.pop_front().is_some(),
976            RelayQueueKind::Tx => self.tx_queue.pop_front().is_some(),
977            RelayQueueKind::Replay => self.replay_queue.pop_front().is_some(),
978            RelayQueueKind::Recent => self.recent_rx.pop_front().is_some(),
979            RelayQueueKind::ReliableRxBuffer => self.pop_reliable_rx_buffered().is_some(),
980            #[cfg(feature = "discovery")]
981            RelayQueueKind::Discovery => self.pop_discovery_route(),
982        }
983    }
984
985    #[cfg(feature = "discovery")]
986    fn pop_discovery_route(&mut self) -> bool {
987        let Some((&side, _)) = self
988            .discovery_routes
989            .iter()
990            .min_by_key(|(_, route)| route.last_seen_ms)
991        else {
992            return false;
993        };
994        self.discovery_routes.remove(&side);
995        Self::queue_budget_warning("topology route evicted because shared queue budget is full");
996        true
997    }
998
999    fn largest_shared_queue(&self) -> Option<RelayQueueKind> {
1000        let candidates = [
1001            (
1002                RelayQueueKind::Rx,
1003                self.rx_queue.bytes_used(),
1004                self.rx_queue.len(),
1005            ),
1006            (
1007                RelayQueueKind::Tx,
1008                self.tx_queue.bytes_used(),
1009                self.tx_queue.len(),
1010            ),
1011            (
1012                RelayQueueKind::Replay,
1013                self.replay_queue.bytes_used(),
1014                self.replay_queue.len(),
1015            ),
1016            (RelayQueueKind::Recent, 0, 0),
1017            (
1018                RelayQueueKind::ReliableRxBuffer,
1019                self.reliable_rx_buffered_bytes(),
1020                self.reliable_rx_buffer_len(),
1021            ),
1022            #[cfg(feature = "discovery")]
1023            (
1024                RelayQueueKind::Discovery,
1025                self.discovery_bytes_used(),
1026                self.discovery_routes.len(),
1027            ),
1028        ];
1029        candidates
1030            .into_iter()
1031            .filter(|(_, bytes, len)| *bytes > 0 && *len > 0)
1032            .max_by_key(|(kind, bytes, _)| {
1033                (
1034                    *bytes,
1035                    if *kind == RelayQueueKind::ReliableRxBuffer {
1036                        0
1037                    } else {
1038                        1
1039                    },
1040                )
1041            })
1042            .map(|(kind, _, _)| kind)
1043    }
1044
1045    fn make_shared_queue_room(
1046        &mut self,
1047        incoming_cost: usize,
1048        preferred: RelayQueueKind,
1049    ) -> TelemetryResult<()> {
1050        if incoming_cost > self.memory.max_queue_budget {
1051            return Err(TelemetryError::PacketTooLarge(
1052                "Item exceeds maximum shared queue budget",
1053            ));
1054        }
1055
1056        while self.shared_queue_bytes_used().saturating_add(incoming_cost)
1057            > self.memory.max_queue_budget
1058        {
1059            let victim = self.largest_shared_queue().unwrap_or(preferred);
1060            if victim == RelayQueueKind::Discovery {
1061                Self::queue_budget_warning("topology data is using the largest queue budget share");
1062            }
1063            if !self.pop_shared_queue_item(victim) && !self.pop_shared_queue_item(preferred) {
1064                return Err(TelemetryError::PacketTooLarge(
1065                    "Item exceeds maximum shared queue budget",
1066                ));
1067            }
1068        }
1069
1070        Ok(())
1071    }
1072
1073    #[inline]
1074    fn queue_budget_warning(msg: &str) {
1075        #[cfg(feature = "std")]
1076        eprintln!("sedsnet queue budget warning: {msg}");
1077        let _ = msg;
1078    }
1079
1080    #[cfg(feature = "discovery")]
1081    fn fit_discovery_budget(&mut self) {
1082        while self.shared_queue_bytes_used() > self.memory.max_queue_budget {
1083            if !self.pop_discovery_route() {
1084                break;
1085            }
1086        }
1087    }
1088
1089    fn push_rx(&mut self, item: RelayRxItem) -> TelemetryResult<()> {
1090        self.make_shared_queue_room(item.byte_cost(), RelayQueueKind::Rx)?;
1091        self.rx_queue
1092            .push_back_prioritized(item, |queued| queued.priority)
1093    }
1094
1095    fn push_tx(&mut self, item: RelayTxItem) -> TelemetryResult<()> {
1096        #[cfg(feature = "discovery")]
1097        {
1098            let incoming = match &item.data {
1099                RelayItem::Packet(pkt) if discovery::is_discovery_type(pkt.data_type()) => {
1100                    Some((pkt.data_type(), pkt.sender().to_owned()))
1101                }
1102                RelayItem::Packet(_) | RelayItem::Packed(_) => None,
1103            };
1104            if let Some((incoming_ty, incoming_sender)) = incoming {
1105                self.tx_queue.retain(|queued| {
1106                    let same_snapshot = match &queued.data {
1107                        RelayItem::Packet(pkt) => {
1108                            pkt.data_type() == incoming_ty && pkt.sender() == incoming_sender
1109                        }
1110                        RelayItem::Packed(_) => false,
1111                    };
1112                    queued.dst != item.dst || !same_snapshot
1113                });
1114            }
1115        }
1116        self.make_shared_queue_room(item.byte_cost(), RelayQueueKind::Tx)?;
1117        self.tx_queue
1118            .push_back_prioritized(item, |queued| queued.priority)
1119    }
1120
1121    fn push_replay(&mut self, item: RelayReplayItem) -> TelemetryResult<()> {
1122        self.make_shared_queue_room(item.byte_cost(), RelayQueueKind::Replay)?;
1123        self.replay_queue
1124            .push_back_prioritized(item, |queued| queued.priority)
1125    }
1126
1127    fn push_recent_rx(&mut self, id: u64) -> TelemetryResult<()> {
1128        while self.recent_rx.len() >= self.memory.max_recent_rx_ids {
1129            let _ = self.recent_rx.pop_front();
1130        }
1131        self.make_shared_queue_room(0, RelayQueueKind::Recent)?;
1132        self.recent_rx.push_back(id)
1133    }
1134
1135    fn buffer_reliable_rx(
1136        &mut self,
1137        side: RelaySideId,
1138        ty: crate::DataType,
1139        seq: u32,
1140        bytes: Arc<[u8]>,
1141    ) -> TelemetryResult<()> {
1142        let key = Relay::reliable_key(side, ty);
1143        if self
1144            .reliable_rx
1145            .get(&key)
1146            .is_some_and(|state| state.buffered.contains_key(&seq))
1147        {
1148            return Ok(());
1149        }
1150        let cost = size_of::<Arc<[u8]>>() + bytes.len();
1151        self.make_shared_queue_room(cost, RelayQueueKind::ReliableRxBuffer)?;
1152        let rx_state = self
1153            .reliable_rx
1154            .entry(key)
1155            .or_insert_with(|| ReliableRxState {
1156                expected_seq: 1,
1157                buffered: BTreeMap::new(),
1158            });
1159        if rx_state.buffered.len() >= runtime_reliable_max_pending() {
1160            let _ = rx_state.buffered.pop_first();
1161        }
1162        rx_state.buffered.insert(seq, bytes);
1163        Ok(())
1164    }
1165}
1166
1167/// Relay that fans out packets from one side to all others.
1168/// - Supports both packed bytes and full Packet.
1169/// - Has RX & TX queues, like Router.
1170/// - Uses a Clock for the *_with_timeout APIs, same style as Router.
1171pub struct Relay {
1172    sender: RouterMutex<Arc<str>>,
1173    state: RouterMutex<RelayInner>,
1174    side_tx_gate: ReentryGate,
1175    clock: Box<dyn Clock + Send + Sync>,
1176}
1177
1178enum RemoteSidePlan {
1179    Target(Vec<RelaySideId>),
1180}
1181
1182impl Relay {
1183    const END_TO_END_ACK_SENDER: &'static str = "E2EACK";
1184    const END_TO_END_ACK_PREFIX: &'static str = "E2EACK:";
1185
1186    fn relay_item_priority(data: &RelayItem) -> TelemetryResult<u8> {
1187        let ty = match data {
1188            RelayItem::Packet(pkt) => pkt.data_type(),
1189            RelayItem::Packed(bytes) => wire_format::peek_envelope(bytes.as_ref())?.ty,
1190        };
1191        Ok(crate::scheduler_priority(ty))
1192    }
1193
1194    #[inline]
1195    fn is_side_tx_busy(err: &TelemetryError) -> bool {
1196        matches!(err, TelemetryError::Io("side tx busy"))
1197    }
1198
1199    fn process_replay_queue_item(&self) -> TelemetryResult<bool> {
1200        let Some(item) = ({
1201            let mut st = self.state.lock();
1202            st.replay_queue.pop_front()
1203        }) else {
1204            return Ok(false);
1205        };
1206        let frame = wire_format::peek_frame_info(item.bytes.as_ref())?;
1207        let ty = frame.envelope.ty;
1208        let Some(hdr) = frame.reliable else {
1209            return Ok(false);
1210        };
1211        {
1212            let mut st = self.state.lock();
1213            let tx_state = self.reliable_tx_state_mut(&mut st, item.dst, ty);
1214            if !tx_state.sent.contains_key(&hdr.seq) {
1215                return Ok(false);
1216            }
1217        }
1218        if let Err(e) = self.send_reliable_raw_to_side(item.dst, item.bytes.clone()) {
1219            if Self::is_side_tx_busy(&e) {
1220                let mut st = self.state.lock();
1221                st.push_replay(item)?;
1222                return Ok(false);
1223            }
1224            return Err(e);
1225        }
1226        let mut st = self.state.lock();
1227        let tx_state = self.reliable_tx_state_mut(&mut st, item.dst, ty);
1228        if let Some(sent) = tx_state.sent.get_mut(&hdr.seq) {
1229            sent.last_send_ms = self.clock.now_ms();
1230            sent.queued = false;
1231        }
1232        Ok(true)
1233    }
1234
1235    fn pop_ready_tx_item(
1236        &self,
1237    ) -> Option<(
1238        Option<RelaySideId>,
1239        RelaySideId,
1240        RelayTxHandlerFn,
1241        RelaySideOptions,
1242        RelayItem,
1243    )> {
1244        let mut st = self.state.lock();
1245        let mut priority_burst = st.tx_priority_burst;
1246        let item = st
1247            .tx_queue
1248            .pop_front_fair(&mut priority_burst, MAX_PRIORITY_BURST, |queued| {
1249                queued.priority
1250            });
1251        st.tx_priority_burst = priority_burst;
1252        if let Some(item) = item {
1253            let side = st.sides.get(item.dst).and_then(|side| side.clone());
1254            side.map(|s| (item.src, item.dst, s.tx_handler, s.opts, item.data))
1255        } else {
1256            None
1257        }
1258    }
1259
1260    fn send_tx_item(
1261        &self,
1262        src: Option<RelaySideId>,
1263        dst: RelaySideId,
1264        handler: RelayTxHandlerFn,
1265        opts: RelaySideOptions,
1266        data: RelayItem,
1267    ) -> TelemetryResult<bool> {
1268        let allowed = {
1269            let mut st = self.state.lock();
1270            let ty = match &data {
1271                RelayItem::Packet(pkt) => Some(pkt.data_type()),
1272                RelayItem::Packed(bytes) => Some(wire_format::peek_envelope(bytes.as_ref())?.ty),
1273            };
1274            let route_allowed = self.route_allowed_locked(&st, src, ty, dst);
1275            #[cfg(all(feature = "discovery", feature = "timesync"))]
1276            let timesync_allowed = ty
1277                .map(|ty| {
1278                    Self::timesync_allowed_for_side_locked(&mut st, dst, ty, self.clock.now_ms())
1279                })
1280                .unwrap_or(true);
1281            #[cfg(not(all(feature = "discovery", feature = "timesync")))]
1282            let timesync_allowed = true;
1283            route_allowed && timesync_allowed
1284        };
1285        if !allowed {
1286            return Ok(false);
1287        }
1288        if opts.reliable_enabled && matches!(handler, RelayTxHandlerFn::Packed(_)) {
1289            self.send_reliable_to_side(dst, data)?;
1290            Ok(true)
1291        } else if let Some(adjusted) = self.adjust_reliable_for_side(opts, data)? {
1292            self.call_tx_handler(dst, &handler, &adjusted)?;
1293            Ok(true)
1294        } else {
1295            Ok(false)
1296        }
1297    }
1298
1299    /// Create a new relay with the given clock.
1300    pub fn new(clock: Box<dyn Clock + Send + Sync>) -> Self {
1301        Self::new_with_config(RelayConfig::default(), clock)
1302    }
1303
1304    /// Create a new relay with explicit runtime configuration and clock.
1305    pub fn new_with_config(cfg: RelayConfig, clock: Box<dyn Clock + Send + Sync>) -> Self {
1306        let memory = cfg.memory_config();
1307        Self {
1308            sender: RouterMutex::new(cfg.sender()),
1309            state: RouterMutex::new(RelayInner {
1310                memory,
1311                sides: Vec::new(),
1312                route_overrides: BTreeMap::new(),
1313                typed_route_overrides: BTreeMap::new(),
1314                route_weights: BTreeMap::new(),
1315                route_priorities: BTreeMap::new(),
1316                source_route_modes: BTreeMap::new(),
1317                route_selection_cursors: BTreeMap::new(),
1318                adaptive_route_stats: BTreeMap::new(),
1319                side_runtime_stats: BTreeMap::new(),
1320                side_transport: BTreeMap::new(),
1321                rx_queue: BoundedDeque::new(
1322                    memory.max_queue_budget,
1323                    memory.starting_queue_size,
1324                    memory.queue_grow_step,
1325                ),
1326                tx_queue: BoundedDeque::new(
1327                    memory.max_queue_budget,
1328                    memory.starting_queue_size,
1329                    memory.queue_grow_step,
1330                ),
1331                tx_priority_burst: 0,
1332                replay_queue: BoundedDeque::new(
1333                    memory.max_queue_budget,
1334                    memory.starting_queue_size,
1335                    memory.queue_grow_step,
1336                ),
1337                recent_rx: BoundedDeque::new(
1338                    memory.recent_rx_queue_bytes(),
1339                    memory.recent_rx_queue_bytes(),
1340                    memory.queue_grow_step,
1341                ),
1342                reliable_tx: BTreeMap::new(),
1343                reliable_rx: BTreeMap::new(),
1344                reliable_return_routes: BTreeMap::new(),
1345                reliable_return_route_order: VecDeque::new(),
1346                end_to_end_acked_destinations: BTreeMap::new(),
1347                end_to_end_acked_destination_order: VecDeque::new(),
1348                total_handler_failures: 0,
1349                total_handler_retries: 0,
1350                #[cfg(feature = "discovery")]
1351                discovery_routes: BTreeMap::new(),
1352                #[cfg(feature = "discovery")]
1353                discovery_cadence: DiscoveryCadenceState::default(),
1354                #[cfg(feature = "discovery")]
1355                discovery_side_throttle: BTreeMap::new(),
1356                #[cfg(all(feature = "discovery", feature = "timesync"))]
1357                timesync_side_throttle: BTreeMap::new(),
1358            }),
1359            side_tx_gate: ReentryGate::new(),
1360            clock,
1361        }
1362    }
1363
1364    #[inline]
1365    fn sender_arc(&self) -> Arc<str> {
1366        self.sender.lock().clone()
1367    }
1368
1369    #[inline]
1370    pub fn sender(&self) -> Arc<str> {
1371        self.sender_arc()
1372    }
1373
1374    pub fn set_sender<S: AsRef<str>>(&self, sender: S) {
1375        *self.sender.lock() = Arc::from(sender.as_ref());
1376    }
1377
1378    #[inline]
1379    fn try_enter_side_tx(&self) -> Option<ReentryGuard<'_>> {
1380        self.side_tx_gate.try_enter()
1381    }
1382
1383    #[inline]
1384    fn side_tx_active(&self) -> bool {
1385        self.side_tx_gate.is_active()
1386    }
1387
1388    #[inline]
1389    fn side_ref(st: &RelayInner, side: RelaySideId) -> TelemetryResult<&RelaySide> {
1390        st.sides
1391            .get(side)
1392            .and_then(|side| side.as_ref())
1393            .ok_or(TelemetryError::HandlerError("relay: invalid side id"))
1394    }
1395
1396    fn note_side_tx_success(
1397        &self,
1398        side: RelaySideId,
1399        ty: crate::DataType,
1400        bytes: usize,
1401        attempts: usize,
1402    ) {
1403        let mut st = self.state.lock();
1404        let entry = st.side_runtime_stats.entry(side).or_default();
1405        entry.note_tx(ty, bytes, attempts.saturating_sub(1));
1406    }
1407
1408    fn note_side_tx_failure(&self, side: RelaySideId, ty: crate::DataType, attempts: usize) {
1409        let mut st = self.state.lock();
1410        st.total_handler_failures = st.total_handler_failures.saturating_add(1);
1411        st.total_handler_retries = st.total_handler_retries.saturating_add(attempts as u64);
1412        let entry = st.side_runtime_stats.entry(side).or_default();
1413        entry.note_tx_failure(ty, attempts);
1414    }
1415
1416    fn note_side_rx(&self, side: RelaySideId, ty: crate::DataType, bytes: usize) {
1417        let mut st = self.state.lock();
1418        let entry = st.side_runtime_stats.entry(side).or_default();
1419        entry.note_rx(ty, bytes);
1420    }
1421
1422    #[inline]
1423    fn ensure_side_ingress_enabled(&self, side: RelaySideId) -> TelemetryResult<()> {
1424        let st = self.state.lock();
1425        let side_ref = Self::side_ref(&st, side)?;
1426        if side_ref.opts.ingress_enabled {
1427            Ok(())
1428        } else {
1429            Err(TelemetryError::HandlerError(
1430                "relay: ingress disabled for side id",
1431            ))
1432        }
1433    }
1434
1435    #[inline]
1436    fn route_allowed_locked(
1437        &self,
1438        st: &RelayInner,
1439        src: Option<RelaySideId>,
1440        ty: Option<crate::DataType>,
1441        dst: RelaySideId,
1442    ) -> bool {
1443        let Ok(dst_side) = Self::side_ref(st, dst) else {
1444            return false;
1445        };
1446        if !dst_side.opts.egress_enabled {
1447            return false;
1448        }
1449        if let Some(src_id) = src {
1450            let Ok(src_side) = Self::side_ref(st, src_id) else {
1451                return false;
1452            };
1453            if !src_side.opts.ingress_enabled || src_id == dst {
1454                return false;
1455            }
1456        }
1457        let base_allowed = st.route_overrides.get(&(src, dst)).copied().unwrap_or(true);
1458        if !base_allowed {
1459            return false;
1460        }
1461
1462        let Some(ty) = ty else {
1463            return true;
1464        };
1465        if st
1466            .typed_route_overrides
1467            .keys()
1468            .any(|(typed_src, typed_ty, _)| *typed_src == src && *typed_ty == ty.as_u32())
1469        {
1470            return st
1471                .typed_route_overrides
1472                .get(&(src, ty.as_u32(), dst))
1473                .copied()
1474                .unwrap_or(false);
1475        }
1476        true
1477    }
1478
1479    fn has_typed_route_overrides_locked(
1480        st: &RelayInner,
1481        src: Option<RelaySideId>,
1482        ty: crate::DataType,
1483    ) -> bool {
1484        st.typed_route_overrides
1485            .keys()
1486            .any(|(typed_src, typed_ty, _)| *typed_src == src && *typed_ty == ty.as_u32())
1487    }
1488
1489    fn eligible_side_ids_locked(
1490        &self,
1491        st: &RelayInner,
1492        src: Option<RelaySideId>,
1493        ty: Option<crate::DataType>,
1494        restrict_link_local: bool,
1495    ) -> Vec<RelaySideId> {
1496        st.sides
1497            .iter()
1498            .enumerate()
1499            .filter_map(|(side_id, side)| {
1500                let side = side.as_ref()?;
1501                if restrict_link_local && !side.opts.link_local_enabled {
1502                    return None;
1503                }
1504                if self.route_allowed_locked(st, src, ty, side_id) {
1505                    Some(side_id)
1506                } else {
1507                    None
1508                }
1509            })
1510            .collect()
1511    }
1512
1513    fn apply_route_selection_locked(
1514        &self,
1515        st: &mut RelayInner,
1516        src: Option<RelaySideId>,
1517        mut sides: Vec<RelaySideId>,
1518        origin: RouteSelectionOrigin,
1519    ) -> Vec<RelaySideId> {
1520        if sides.len() <= 1 {
1521            return sides;
1522        }
1523
1524        let selection_mode = st.source_route_modes.get(&src).copied();
1525        if selection_mode.is_none() && origin == RouteSelectionOrigin::Discovered {
1526            return self.apply_adaptive_discovery_selection_locked(st, src, sides);
1527        }
1528
1529        match selection_mode.unwrap_or(RouteSelectionMode::Fanout) {
1530            RouteSelectionMode::Fanout => sides,
1531            RouteSelectionMode::Weighted => {
1532                sides.sort_unstable();
1533                let total_weight = sides.iter().fold(0_u64, |acc, side| {
1534                    acc + u64::from(st.route_weights.get(&(src, *side)).copied().unwrap_or(1))
1535                });
1536                if total_weight == 0 {
1537                    return Vec::new();
1538                }
1539                let cursor = st.route_selection_cursors.entry(src).or_insert(0);
1540                let pick = *cursor % total_weight;
1541                *cursor = cursor.wrapping_add(1);
1542                let mut remaining = pick;
1543                for side in sides {
1544                    let weight =
1545                        u64::from(st.route_weights.get(&(src, side)).copied().unwrap_or(1));
1546                    if remaining < weight {
1547                        return vec![side];
1548                    }
1549                    remaining -= weight;
1550                }
1551                Vec::new()
1552            }
1553            RouteSelectionMode::Failover => {
1554                sides.sort_by_key(|side| {
1555                    (
1556                        st.route_priorities.get(&(src, *side)).copied().unwrap_or(0),
1557                        *side,
1558                    )
1559                });
1560                sides.truncate(1);
1561                sides
1562            }
1563        }
1564    }
1565
1566    fn apply_adaptive_discovery_selection_locked(
1567        &self,
1568        st: &mut RelayInner,
1569        src: Option<RelaySideId>,
1570        mut sides: Vec<RelaySideId>,
1571    ) -> Vec<RelaySideId> {
1572        sides.sort_unstable();
1573        let mut unmeasured: Vec<_> = sides
1574            .iter()
1575            .copied()
1576            .filter(|side| !st.adaptive_route_stats.contains_key(side))
1577            .collect();
1578        if !unmeasured.is_empty() {
1579            let cursor = st.route_selection_cursors.entry(src).or_insert(0);
1580            let pick = (*cursor as usize) % unmeasured.len();
1581            *cursor = cursor.wrapping_add(1);
1582            return vec![unmeasured.swap_remove(pick)];
1583        }
1584
1585        let now_ms = self.clock.now_ms();
1586        let total_weight = sides.iter().fold(0_u64, |acc, side| {
1587            acc + st
1588                .adaptive_route_stats
1589                .get(side)
1590                .map(|stats| stats.weight(now_ms))
1591                .unwrap_or(1)
1592        });
1593        if total_weight == 0 {
1594            sides.truncate(1);
1595            return sides;
1596        }
1597
1598        let cursor = st.route_selection_cursors.entry(src).or_insert(0);
1599        let pick = *cursor % total_weight;
1600        *cursor = cursor.wrapping_add(1);
1601        let mut remaining = pick;
1602        for side in sides {
1603            let weight = st
1604                .adaptive_route_stats
1605                .get(&side)
1606                .map(|stats| stats.weight(now_ms))
1607                .unwrap_or(1);
1608            if remaining < weight {
1609                return vec![side];
1610            }
1611            remaining -= weight;
1612        }
1613        Vec::new()
1614    }
1615
1616    /// Prefer the discovery side with the shortest advertised topology path
1617    /// to the packet's destination. This prevents a route reflected through
1618    /// another router from competing equally with the direct route.
1619    #[cfg(feature = "discovery")]
1620    fn retain_shortest_discovery_routes_locked(
1621        st: &RelayInner,
1622        sides: &mut Vec<RelaySideId>,
1623        endpoints: &[crate::DataEndpoint],
1624        now_ms: u64,
1625    ) {
1626        if sides.len() <= 1 || endpoints.is_empty() {
1627            return;
1628        }
1629
1630        let route_distance = |side: RelaySideId| -> Option<usize> {
1631            let route = st.discovery_routes.get(&side)?;
1632            route
1633                .announcers
1634                .iter()
1635                .filter(|(_, state)| {
1636                    now_ms.saturating_sub(state.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
1637                })
1638                .filter_map(|(announcer, state)| {
1639                    let targets: BTreeSet<&str> = state
1640                        .topology_boards
1641                        .iter()
1642                        .filter(|board| {
1643                            endpoints
1644                                .iter()
1645                                .any(|endpoint| board.reachable_endpoints.contains(endpoint))
1646                        })
1647                        .map(|board| board.sender_id.as_str())
1648                        .collect();
1649                    if targets.is_empty() {
1650                        return state
1651                            .reachable
1652                            .iter()
1653                            .any(|endpoint| endpoints.contains(endpoint))
1654                            .then_some(usize::MAX / 2);
1655                    }
1656
1657                    let mut distances: BTreeMap<&str, usize> = BTreeMap::new();
1658                    let mut pending = VecDeque::from([(announcer.as_str(), 0usize)]);
1659                    while let Some((sender, distance)) = pending.pop_front() {
1660                        if distances.contains_key(sender) {
1661                            continue;
1662                        }
1663                        distances.insert(sender, distance);
1664                        if targets.contains(sender) {
1665                            return Some(distance);
1666                        }
1667                        for board in &state.topology_boards {
1668                            if board.sender_id == sender {
1669                                for peer in &board.connections {
1670                                    if !distances.contains_key(peer.as_str()) {
1671                                        pending.push_back((peer.as_str(), distance + 1));
1672                                    }
1673                                }
1674                            } else if board.connections.iter().any(|peer| peer == sender)
1675                                && !distances.contains_key(board.sender_id.as_str())
1676                            {
1677                                pending.push_back((board.sender_id.as_str(), distance + 1));
1678                            }
1679                        }
1680                    }
1681                    None
1682                })
1683                .min()
1684        };
1685
1686        let distances: Vec<(RelaySideId, usize)> = sides
1687            .iter()
1688            .filter_map(|side| route_distance(*side).map(|distance| (*side, distance)))
1689            .collect();
1690        let Some(best) = distances.iter().map(|(_, distance)| *distance).min() else {
1691            return;
1692        };
1693        sides.retain(|side| {
1694            distances
1695                .iter()
1696                .any(|(candidate, distance)| candidate == side && *distance == best)
1697        });
1698    }
1699
1700    fn record_side_tx_sample(
1701        &self,
1702        side: RelaySideId,
1703        bytes: usize,
1704        started_ms: u64,
1705        ended_ms: u64,
1706    ) {
1707        let sample_ms = ended_ms.saturating_sub(started_ms).max(1);
1708        let sample_bps = ((bytes as u128).saturating_mul(1000) / u128::from(sample_ms))
1709            .min(u128::from(u64::MAX)) as u64;
1710        let mut st = self.state.lock();
1711        st.adaptive_route_stats
1712            .entry(side)
1713            .or_default()
1714            .observe(bytes, sample_bps, ended_ms);
1715    }
1716
1717    /// Seed adaptive route selection with a transport-measured link probe.
1718    ///
1719    /// Call this after a side-specific bring-up probe, or whenever the transport already knows the
1720    /// duration for a frame. The relay does not emit synthetic probe frames by itself.
1721    pub fn note_side_link_probe_sample(
1722        &self,
1723        side: RelaySideId,
1724        bytes: usize,
1725        duration_ms: u64,
1726    ) -> TelemetryResult<()> {
1727        {
1728            let st = self.state.lock();
1729            let _ = Self::side_ref(&st, side).map_err(|_| TelemetryError::BadArg)?;
1730        }
1731        let ended_ms = self.clock.now_ms();
1732        self.record_side_tx_sample(side, bytes, ended_ms.saturating_sub(duration_ms), ended_ms);
1733        Ok(())
1734    }
1735
1736    fn relay_item_wire_len(data: &RelayItem) -> TelemetryResult<usize> {
1737        match data {
1738            RelayItem::Packet(pkt) => Ok(wire_format::pack_packet(pkt).len()),
1739            RelayItem::Packed(bytes) => Ok(bytes.len()),
1740        }
1741    }
1742
1743    #[inline]
1744    fn decode_end_to_end_reliable_ack(payload: &[u8]) -> TelemetryResult<u64> {
1745        if payload.len() != 8 {
1746            return Err(TelemetryError::Unpack("bad reliable e2e ack payload"));
1747        }
1748        Ok(u64::from_le_bytes(payload[0..8].try_into().unwrap()))
1749    }
1750
1751    #[inline]
1752    fn is_end_to_end_ack_sender(sender: &str) -> bool {
1753        sender == Self::END_TO_END_ACK_SENDER || sender.starts_with(Self::END_TO_END_ACK_PREFIX)
1754    }
1755
1756    #[inline]
1757    fn sender_hash(sender: &str) -> u64 {
1758        if let Some(address) = sender
1759            .strip_prefix("@addr:")
1760            .and_then(|value| value.parse::<u32>().ok())
1761        {
1762            return u64::from(address);
1763        }
1764        hash_bytes_u64(0x517C_C1B7_2722_0A95, sender.as_bytes())
1765    }
1766
1767    #[cfg(feature = "discovery")]
1768    fn canonical_sender_locked(st: &RelayInner, sender: &str) -> String {
1769        let Some(address) = sender
1770            .strip_prefix("@addr:")
1771            .and_then(|value| value.parse::<u32>().ok())
1772        else {
1773            return sender.to_string();
1774        };
1775        st.discovery_routes
1776            .values()
1777            .flat_map(|route| route.announcers.iter())
1778            .flat_map(|(announcer, state)| {
1779                core::iter::once(announcer.as_str()).chain(
1780                    state
1781                        .topology_boards
1782                        .iter()
1783                        .map(|board| board.sender_id.as_str()),
1784                )
1785            })
1786            .find(|candidate| sender_address_u32(candidate) == address)
1787            .map(ToString::to_string)
1788            .unwrap_or_else(|| sender.to_string())
1789    }
1790
1791    fn decode_end_to_end_ack_sender_hash(sender: &str) -> Option<u64> {
1792        sender
1793            .strip_prefix(Self::END_TO_END_ACK_PREFIX)
1794            .filter(|sender| !sender.is_empty())
1795            .map(Self::sender_hash)
1796    }
1797
1798    #[cfg(feature = "discovery")]
1799    fn is_end_to_end_destination_sender(&self, sender: &str) -> bool {
1800        sender != self.sender_arc().as_ref() && !Self::is_end_to_end_ack_sender(sender)
1801    }
1802
1803    /// Extract the logical packet ID targeted by an end-to-end reliable ACK item.
1804    ///
1805    /// Relay queues can hold either decoded packets or packed frames. This
1806    /// helper normalizes both forms so relay ACK-routing logic can treat them
1807    /// uniformly.
1808    ///
1809    /// Only relay-visible end-to-end `ReliableAck` packets qualify here.
1810    /// Unrelated traffic returns `Ok(None)`.
1811    fn reliable_control_target_packet_id(data: &RelayItem) -> TelemetryResult<Option<u64>> {
1812        match data {
1813            RelayItem::Packet(pkt) => {
1814                if pkt.data_type() != crate::DataType::ReliableAck
1815                    || !Self::is_end_to_end_ack_sender(pkt.sender())
1816                {
1817                    return Ok(None);
1818                }
1819                Self::decode_end_to_end_reliable_ack(pkt.payload()).map(Some)
1820            }
1821            RelayItem::Packed(bytes) => {
1822                if wire_format::peek_frame_info(bytes.as_ref())
1823                    .ok()
1824                    .is_some_and(|frame| frame.ack_only())
1825                {
1826                    return Ok(None);
1827                }
1828                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
1829                if pkt.data_type() != crate::DataType::ReliableAck
1830                    || !Self::is_end_to_end_ack_sender(pkt.sender())
1831                {
1832                    return Ok(None);
1833                }
1834                Self::decode_end_to_end_reliable_ack(pkt.payload()).map(Some)
1835            }
1836        }
1837    }
1838
1839    fn note_reliable_return_route(&self, side: RelaySideId, packet_id: u64) {
1840        let mut st = self.state.lock();
1841        Self::remember_reliable_return_route_locked(&mut st, packet_id);
1842        st.reliable_return_routes
1843            .insert(packet_id, ReliableReturnRouteState { side });
1844    }
1845
1846    /// Refresh or insert `packet_id` in the bounded reliable return-route cache.
1847    ///
1848    /// The relay uses this cache to route end-to-end acknowledgements back
1849    /// toward the source side that most recently forwarded the corresponding
1850    /// reliable data packet.
1851    fn remember_reliable_return_route_locked(st: &mut RelayInner, packet_id: u64) {
1852        let cap = runtime_reliable_max_return_routes().max(1);
1853        st.reliable_return_route_order
1854            .retain(|id| st.reliable_return_routes.contains_key(id) && *id != packet_id);
1855        while st.reliable_return_route_order.len() >= cap {
1856            if let Some(oldest) = st.reliable_return_route_order.pop_front() {
1857                st.reliable_return_routes.remove(&oldest);
1858            } else {
1859                break;
1860            }
1861        }
1862        st.reliable_return_route_order.push_back(packet_id);
1863    }
1864
1865    fn note_end_to_end_acked_destination_locked(
1866        st: &mut RelayInner,
1867        packet_id: u64,
1868        sender_hash: u64,
1869    ) {
1870        let entry_cap = runtime_reliable_max_end_to_end_ack_cache().max(1);
1871        st.end_to_end_acked_destination_order
1872            .retain(|id| st.end_to_end_acked_destinations.contains_key(id) && *id != packet_id);
1873        while st.end_to_end_acked_destination_order.len() >= entry_cap {
1874            if let Some(oldest) = st.end_to_end_acked_destination_order.pop_front() {
1875                st.end_to_end_acked_destinations.remove(&oldest);
1876            } else {
1877                break;
1878            }
1879        }
1880        st.end_to_end_acked_destination_order.push_back(packet_id);
1881
1882        let acked = st
1883            .end_to_end_acked_destinations
1884            .entry(packet_id)
1885            .or_default();
1886        let sender_cap = runtime_reliable_max_end_to_end_pending().max(1);
1887        if acked.len() < sender_cap || acked.contains(&sender_hash) {
1888            acked.insert(sender_hash);
1889        }
1890    }
1891
1892    #[inline]
1893    fn reliable_key(side: RelaySideId, ty: crate::DataType) -> (RelaySideId, u32) {
1894        (side, ty.as_u32())
1895    }
1896
1897    fn reliable_tx_state_mut<'a>(
1898        &'a self,
1899        st: &'a mut RelayInner,
1900        side: RelaySideId,
1901        ty: crate::DataType,
1902    ) -> &'a mut ReliableTxState {
1903        let key = Self::reliable_key(side, ty);
1904        st.reliable_tx
1905            .entry(key)
1906            .or_insert_with(|| ReliableTxState {
1907                next_seq: 1,
1908                sent_order: VecDeque::new(),
1909                sent: BTreeMap::new(),
1910            })
1911    }
1912
1913    fn reliable_rx_state_mut<'a>(
1914        &'a self,
1915        st: &'a mut RelayInner,
1916        side: RelaySideId,
1917        ty: crate::DataType,
1918    ) -> &'a mut ReliableRxState {
1919        let key = Self::reliable_key(side, ty);
1920        st.reliable_rx
1921            .entry(key)
1922            .or_insert_with(|| ReliableRxState {
1923                expected_seq: 1,
1924                buffered: BTreeMap::new(),
1925            })
1926    }
1927
1928    fn handle_reliable_ack(&self, side: RelaySideId, ty: crate::DataType, ack: u32) {
1929        let mut st = self.state.lock();
1930        let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
1931        if matches!(reliable_mode(ty), crate::ReliableMode::Unordered) {
1932            tx_state.sent.remove(&ack);
1933            tx_state.sent_order.retain(|seq| *seq != ack);
1934            return;
1935        }
1936
1937        while let Some(seq) = tx_state.sent_order.front().copied() {
1938            if seq > ack {
1939                break;
1940            }
1941            tx_state.sent_order.pop_front();
1942            tx_state.sent.remove(&seq);
1943        }
1944    }
1945
1946    fn handle_reliable_partial_ack(&self, side: RelaySideId, ty: crate::DataType, seq: u32) {
1947        let mut st = self.state.lock();
1948        let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
1949        if let Some(sent) = tx_state.sent.get_mut(&seq) {
1950            sent.partial_acked = true;
1951        }
1952    }
1953
1954    fn reliable_control_packet(
1955        &self,
1956        control_ty: crate::DataType,
1957        ty: crate::DataType,
1958        seq: u32,
1959    ) -> TelemetryResult<Packet> {
1960        let sender = self.sender_arc();
1961        Packet::new(
1962            control_ty,
1963            message_meta(control_ty).endpoints_ref(),
1964            sender.as_ref(),
1965            self.clock.now_ms(),
1966            crate::router::encode_slice_le(&[ty.as_u32(), seq]),
1967        )
1968    }
1969
1970    fn queue_reliable_ack(
1971        &self,
1972        side: RelaySideId,
1973        ty: crate::DataType,
1974        seq: u32,
1975    ) -> TelemetryResult<()> {
1976        let pkt = self.reliable_control_packet(crate::DataType::ReliableAck, ty, seq)?;
1977        let data = RelayItem::Packet(Arc::new(pkt));
1978        let priority = Self::relay_item_priority(&data)?;
1979        let mut st = self.state.lock();
1980        st.push_tx(RelayTxItem {
1981            src: None,
1982            dst: side,
1983            data,
1984            priority,
1985        })?;
1986        Ok(())
1987    }
1988
1989    fn queue_reliable_packet_request(
1990        &self,
1991        side: RelaySideId,
1992        ty: crate::DataType,
1993        seq: u32,
1994    ) -> TelemetryResult<()> {
1995        let pkt = self.reliable_control_packet(crate::DataType::ReliablePacketRequest, ty, seq)?;
1996        let data = RelayItem::Packet(Arc::new(pkt));
1997        let priority = Self::relay_item_priority(&data)?;
1998        let mut st = self.state.lock();
1999        st.push_tx(RelayTxItem {
2000            src: None,
2001            dst: side,
2002            data,
2003            priority,
2004        })?;
2005        Ok(())
2006    }
2007
2008    fn queue_reliable_partial_ack(
2009        &self,
2010        side: RelaySideId,
2011        ty: crate::DataType,
2012        seq: u32,
2013    ) -> TelemetryResult<()> {
2014        let pkt = self.reliable_control_packet(crate::DataType::ReliablePartialAck, ty, seq)?;
2015        let data = RelayItem::Packet(Arc::new(pkt));
2016        let priority = Self::relay_item_priority(&data)?;
2017        let mut st = self.state.lock();
2018        st.push_tx(RelayTxItem {
2019            src: None,
2020            dst: side,
2021            data,
2022            priority,
2023        })?;
2024        Ok(())
2025    }
2026
2027    fn queue_reliable_retransmit(
2028        &self,
2029        side: RelaySideId,
2030        ty: crate::DataType,
2031        seq: u32,
2032    ) -> TelemetryResult<()> {
2033        let mut queued = None;
2034        {
2035            let mut st = self.state.lock();
2036            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2037            if let Some(sent) = tx_state.sent.get_mut(&seq)
2038                && !sent.queued
2039            {
2040                sent.queued = true;
2041                sent.partial_acked = false;
2042                queued = Some(sent.bytes.clone());
2043            }
2044        }
2045
2046        if let Some(bytes) = queued {
2047            let mut st = self.state.lock();
2048            st.push_replay(RelayReplayItem {
2049                dst: side,
2050                bytes,
2051                priority: message_priority(ty).saturating_add(16),
2052            })?;
2053        }
2054        Ok(())
2055    }
2056
2057    fn send_reliable_raw_to_side(
2058        &self,
2059        side: RelaySideId,
2060        bytes: Arc<[u8]>,
2061    ) -> TelemetryResult<()> {
2062        let (handler, opts) = {
2063            let st = self.state.lock();
2064            let side_ref = Self::side_ref(&st, side)?;
2065            if !side_ref.opts.egress_enabled {
2066                return Ok(());
2067            }
2068            (side_ref.tx_handler.clone(), side_ref.opts)
2069        };
2070
2071        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2072            return Err(TelemetryError::Io("side tx busy"));
2073        };
2074        let started_ms = self.clock.now_ms();
2075        let ty = wire_format::peek_envelope(bytes.as_ref())
2076            .map(|env| env.ty)
2077            .unwrap_or(crate::DataType::ReliableAck);
2078        let result = match handler {
2079            RelayTxHandlerFn::Packed(f) => {
2080                let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2081                let mut sent_bytes = 0usize;
2082                for frame in frames {
2083                    f(frame.as_ref())?;
2084                    sent_bytes = sent_bytes.saturating_add(frame.len());
2085                }
2086                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2087                self.note_side_tx_success(side, ty, sent_bytes, 1);
2088                return Ok(());
2089            }
2090            RelayTxHandlerFn::Packet(f) => {
2091                if wire_format::peek_frame_info(bytes.as_ref())
2092                    .ok()
2093                    .is_some_and(|frame| frame.ack_only())
2094                {
2095                    return Ok(());
2096                }
2097                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
2098                f(&pkt)
2099            }
2100        };
2101        if result.is_ok() {
2102            self.record_side_tx_sample(side, bytes.len(), started_ms, self.clock.now_ms());
2103            self.note_side_tx_success(side, ty, bytes.len(), 1);
2104        } else {
2105            self.note_side_tx_failure(side, ty, 1);
2106        }
2107        result
2108    }
2109
2110    fn send_reliable_to_side(&self, side: RelaySideId, data: RelayItem) -> TelemetryResult<()> {
2111        let (handler, opts, hop_reliable_enabled) = {
2112            let st = self.state.lock();
2113            let side_ref = Self::side_ref(&st, side)?;
2114            let opts = side_ref.opts;
2115            let hop_reliable_enabled = opts.reliable_enabled
2116                && !self.side_has_multiple_announcers_locked(&st, side, self.clock.now_ms());
2117            (side_ref.tx_handler.clone(), opts, hop_reliable_enabled)
2118        };
2119
2120        let RelayTxHandlerFn::Packed(f) = &handler else {
2121            return self.call_tx_handler(side, &handler, &data);
2122        };
2123
2124        if !hop_reliable_enabled {
2125            let mut adjusted_opts = opts;
2126            adjusted_opts.reliable_enabled = false;
2127            if let Some(adjusted) = self.adjust_reliable_for_side(adjusted_opts, data)? {
2128                return self.call_tx_handler(side, &handler, &adjusted);
2129            }
2130            return Ok(());
2131        }
2132
2133        let ty = match &data {
2134            RelayItem::Packet(pkt) => pkt.data_type(),
2135            RelayItem::Packed(bytes) => {
2136                let Ok(frame) = wire_format::peek_frame_info(bytes.as_ref()) else {
2137                    return self.call_tx_handler(side, &handler, &data);
2138                };
2139                frame.envelope.ty
2140            }
2141        };
2142
2143        if !is_reliable_type(ty) {
2144            if let Some(adjusted) = self.adjust_reliable_for_side(opts, data)? {
2145                self.call_tx_handler(side, &handler, &adjusted)?;
2146            }
2147            return Ok(());
2148        }
2149
2150        let (seq, flags) = {
2151            let mut st = self.state.lock();
2152            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2153            if tx_state.sent.len() >= runtime_reliable_max_pending() {
2154                return Err(TelemetryError::PacketTooLarge(
2155                    "relay reliable history full",
2156                ));
2157            }
2158            let seq = tx_state.next_seq;
2159            let next = tx_state.next_seq.wrapping_add(1);
2160            tx_state.next_seq = if next == 0 { 1 } else { next };
2161            let flags = match reliable_mode(ty) {
2162                crate::ReliableMode::Unordered => wire_format::RELIABLE_FLAG_UNORDERED,
2163                _ => 0,
2164            };
2165            (seq, flags)
2166        };
2167
2168        let bytes: Arc<[u8]> = match data {
2169            RelayItem::Packet(pkt) => wire_format::pack_packet_with_reliable(
2170                &pkt,
2171                wire_format::ReliableHeader { flags, seq, ack: 0 },
2172            ),
2173            RelayItem::Packed(bytes) => {
2174                let Some(rewritten) =
2175                    wire_format::rewrite_reliable_header_owned(bytes.as_ref(), flags, seq, 0)?
2176                else {
2177                    let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2178                        return Err(TelemetryError::Io("side tx busy"));
2179                    };
2180                    let started_ms = self.clock.now_ms();
2181                    let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2182                    let mut sent_bytes = 0usize;
2183                    for frame in frames {
2184                        f(frame.as_ref())?;
2185                        sent_bytes = sent_bytes.saturating_add(frame.len());
2186                    }
2187                    self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2188                    self.note_side_tx_success(side, ty, sent_bytes, 1);
2189                    return Ok(());
2190                };
2191                rewritten
2192            }
2193        };
2194
2195        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
2196            return Err(TelemetryError::Io("side tx busy"));
2197        };
2198        let started_ms = self.clock.now_ms();
2199        let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
2200        let mut sent_bytes = 0usize;
2201        for frame in frames {
2202            f(frame.as_ref())?;
2203            sent_bytes = sent_bytes.saturating_add(frame.len());
2204        }
2205        self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
2206        self.note_side_tx_success(side, ty, sent_bytes, 1);
2207
2208        {
2209            let mut st = self.state.lock();
2210            let tx_state = self.reliable_tx_state_mut(&mut st, side, ty);
2211            tx_state.sent_order.push_back(seq);
2212            tx_state.sent.insert(
2213                seq,
2214                ReliableSent {
2215                    bytes: bytes.clone(),
2216                    last_send_ms: self.clock.now_ms(),
2217                    retries: 0,
2218                    queued: false,
2219                    partial_acked: false,
2220                },
2221            );
2222        }
2223
2224        Ok(())
2225    }
2226
2227    fn item_route_info(
2228        &self,
2229        data: &RelayItem,
2230    ) -> TelemetryResult<(Vec<crate::DataEndpoint>, crate::DataType)> {
2231        match data {
2232            RelayItem::Packet(pkt) => {
2233                let mut eps = pkt.endpoints().to_vec();
2234                eps.sort_unstable();
2235                eps.dedup();
2236                Ok((eps, pkt.data_type()))
2237            }
2238            RelayItem::Packed(bytes) => {
2239                let env = wire_format::peek_envelope(bytes.as_ref())?;
2240                let mut eps: Vec<crate::DataEndpoint> = env.endpoints.iter().copied().collect();
2241                eps.sort_unstable();
2242                eps.dedup();
2243                Ok((eps, env.ty))
2244            }
2245        }
2246    }
2247
2248    fn endpoints_are_link_local_only(eps: &[crate::DataEndpoint]) -> bool {
2249        !eps.is_empty() && eps.iter().all(|ep| ep.is_link_local_only())
2250    }
2251
2252    fn item_target_senders(&self, data: &RelayItem) -> TelemetryResult<Arc<[u64]>> {
2253        match data {
2254            RelayItem::Packet(pkt) => Ok(Arc::from(pkt.wire_target_senders())),
2255            RelayItem::Packed(bytes) => {
2256                Ok(wire_format::peek_envelope(bytes.as_ref())?.target_senders)
2257            }
2258        }
2259    }
2260
2261    #[cfg(feature = "discovery")]
2262    fn has_explicit_route_policy_locked(
2263        st: &RelayInner,
2264        src: Option<RelaySideId>,
2265        ty: crate::DataType,
2266    ) -> bool {
2267        st.route_overrides
2268            .keys()
2269            .any(|(route_src, _)| *route_src == src)
2270            || Self::has_typed_route_overrides_locked(st, src, ty)
2271    }
2272
2273    #[cfg(feature = "discovery")]
2274    fn side_matches_target_senders_locked(
2275        st: &RelayInner,
2276        side: RelaySideId,
2277        target_senders: &[u64],
2278        now_ms: u64,
2279    ) -> bool {
2280        st.discovery_routes
2281            .get(&side)
2282            .map(|route| {
2283                if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2284                    return false;
2285                }
2286                route.announcers.iter().any(|(announcer, sender_state)| {
2287                    if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2288                        return false;
2289                    }
2290                    target_senders.contains(&Self::sender_hash(announcer))
2291                        || sender_state
2292                            .advertised_address
2293                            .is_some_and(|address| target_senders.contains(&u64::from(address)))
2294                        || sender_state.topology_boards.iter().any(|board| {
2295                            target_senders.contains(&Self::sender_hash(&board.sender_id))
2296                        })
2297                })
2298            })
2299            .unwrap_or(false)
2300    }
2301
2302    fn remote_side_plan(
2303        &self,
2304        data: &RelayItem,
2305        exclude: RelaySideId,
2306    ) -> TelemetryResult<RemoteSidePlan> {
2307        #[cfg(feature = "discovery")]
2308        {
2309            let (eps, ty) = self.item_route_info(data)?;
2310            let target_senders = self.item_target_senders(data)?;
2311            let preferred_packet_id = Self::reliable_control_target_packet_id(data)?;
2312            if discovery::is_discovery_type(ty) {
2313                let mut st = self.state.lock();
2314                let sides = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
2315                return Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2316                    &mut st,
2317                    Some(exclude),
2318                    sides,
2319                    RouteSelectionOrigin::Flood,
2320                )));
2321            }
2322
2323            #[cfg(feature = "timesync")]
2324            let preferred_timesync_source = self.preferred_timesync_route_source(data, ty)?;
2325            #[cfg(not(feature = "timesync"))]
2326            let preferred_timesync_source: Option<String> = None;
2327            let mut st = self.state.lock();
2328            if let Some(packet_id) = preferred_packet_id {
2329                let target_side = self.allowed_target_side_locked(
2330                    &st,
2331                    exclude,
2332                    ty,
2333                    st.reliable_return_routes
2334                        .get(&packet_id)
2335                        .map(|route| route.side),
2336                );
2337                if let Some(side) = target_side {
2338                    #[cfg(feature = "timesync")]
2339                    if !Self::timesync_allowed_for_side_locked(
2340                        &mut st,
2341                        side,
2342                        ty,
2343                        self.clock.now_ms(),
2344                    ) {
2345                        return Ok(RemoteSidePlan::Target(Vec::new()));
2346                    }
2347                    return Ok(RemoteSidePlan::Target(vec![side]));
2348                }
2349                return Ok(RemoteSidePlan::Target(Vec::new()));
2350            }
2351            let restrict_link_local = Self::endpoints_are_link_local_only(&eps);
2352            let discovered_origin = if is_reliable_type(ty) {
2353                RouteSelectionOrigin::Flood
2354            } else {
2355                RouteSelectionOrigin::Discovered
2356            };
2357            if st.discovery_routes.is_empty() {
2358                let mut fallback = self.eligible_side_ids_locked(
2359                    &st,
2360                    Some(exclude),
2361                    Some(ty),
2362                    restrict_link_local,
2363                );
2364                #[cfg(feature = "timesync")]
2365                {
2366                    fallback = Self::filter_timesync_sides_locked(
2367                        &mut st,
2368                        ty,
2369                        self.clock.now_ms(),
2370                        fallback,
2371                    );
2372                }
2373                return Ok(RemoteSidePlan::Target(if fallback.len() == 1 {
2374                    fallback
2375                } else {
2376                    Vec::new()
2377                }));
2378            }
2379            let now_ms = self.clock.now_ms();
2380            let mut had_exact = false;
2381            let mut exact_targets = Vec::new();
2382            let mut had_known = false;
2383            let mut generic_targets = Vec::new();
2384            let mut unique_endpoint_fallbacks = Vec::new();
2385
2386            for (&side, route) in st.discovery_routes.iter() {
2387                if side == exclude
2388                    || now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS
2389                {
2390                    continue;
2391                }
2392                if restrict_link_local
2393                    && st
2394                        .sides
2395                        .get(side)
2396                        .and_then(|side| side.as_ref())
2397                        .map(|s| !s.opts.link_local_enabled)
2398                        .unwrap_or(true)
2399                {
2400                    continue;
2401                }
2402                if !self.route_allowed_locked(&st, Some(exclude), Some(ty), side) {
2403                    continue;
2404                }
2405                if !target_senders.is_empty() {
2406                    if !Self::side_matches_target_senders_locked(&st, side, &target_senders, now_ms)
2407                    {
2408                        if eps.iter().copied().any(|ep| route.reachable.contains(&ep)) {
2409                            unique_endpoint_fallbacks.push(side);
2410                        }
2411                        continue;
2412                    }
2413                    had_known = true;
2414                    generic_targets.push(side);
2415                    continue;
2416                }
2417                if preferred_timesync_source.as_deref().is_some_and(|source| {
2418                    route.reachable_timesync_sources.iter().any(|s| s == source)
2419                }) {
2420                    had_exact = true;
2421                    exact_targets.push(side);
2422                    continue;
2423                }
2424                if eps.iter().copied().any(|ep| route.reachable.contains(&ep)) {
2425                    had_known = true;
2426                    generic_targets.push(side);
2427                }
2428            }
2429
2430            // Compact/DHCP links can temporarily disagree on whether a frozen
2431            // destination is represented by hostname hash or assigned address.
2432            // Preserve autonomous endpoint routing only when discovery gives
2433            // one unambiguous non-ingress route; never fan an unknown target
2434            // out across multiple sides.
2435            if !target_senders.is_empty() && !had_known && unique_endpoint_fallbacks.len() == 1 {
2436                had_known = true;
2437                generic_targets = unique_endpoint_fallbacks;
2438            }
2439
2440            if had_exact {
2441                #[cfg(feature = "timesync")]
2442                {
2443                    exact_targets = Self::filter_timesync_sides_locked(
2444                        &mut st,
2445                        ty,
2446                        self.clock.now_ms(),
2447                        exact_targets,
2448                    );
2449                }
2450                let targets = self.filter_end_to_end_satisfied_sides_locked(
2451                    &st,
2452                    data,
2453                    exact_targets,
2454                    &eps,
2455                    ty,
2456                )?;
2457                Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2458                    &mut st,
2459                    Some(exclude),
2460                    targets,
2461                    discovered_origin,
2462                )))
2463            } else if had_known {
2464                Self::retain_shortest_discovery_routes_locked(
2465                    &st,
2466                    &mut generic_targets,
2467                    &eps,
2468                    now_ms,
2469                );
2470                #[cfg(feature = "timesync")]
2471                {
2472                    generic_targets = Self::filter_timesync_sides_locked(
2473                        &mut st,
2474                        ty,
2475                        self.clock.now_ms(),
2476                        generic_targets,
2477                    );
2478                }
2479                let targets = self.filter_end_to_end_satisfied_sides_locked(
2480                    &st,
2481                    data,
2482                    generic_targets,
2483                    &eps,
2484                    ty,
2485                )?;
2486                Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2487                    &mut st,
2488                    Some(exclude),
2489                    targets,
2490                    discovered_origin,
2491                )))
2492            } else {
2493                let fallback = self.eligible_side_ids_locked(
2494                    &st,
2495                    Some(exclude),
2496                    Some(ty),
2497                    restrict_link_local,
2498                );
2499                if fallback.len() == 1 {
2500                    return Ok(RemoteSidePlan::Target(fallback));
2501                }
2502                if Self::has_explicit_route_policy_locked(&st, Some(exclude), ty) {
2503                    let mut sides = self.eligible_side_ids_locked(
2504                        &st,
2505                        Some(exclude),
2506                        Some(ty),
2507                        restrict_link_local,
2508                    );
2509                    #[cfg(feature = "timesync")]
2510                    {
2511                        sides = Self::filter_timesync_sides_locked(
2512                            &mut st,
2513                            ty,
2514                            self.clock.now_ms(),
2515                            sides,
2516                        );
2517                    }
2518                    Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2519                        &mut st,
2520                        Some(exclude),
2521                        sides,
2522                        RouteSelectionOrigin::Flood,
2523                    )))
2524                } else {
2525                    Ok(RemoteSidePlan::Target(Vec::new()))
2526                }
2527            }
2528        }
2529        #[cfg(not(feature = "discovery"))]
2530        {
2531            let (_, ty) = self.item_route_info(data)?;
2532            let mut st = self.state.lock();
2533            if let Some(packet_id) = Self::reliable_control_target_packet_id(data)? {
2534                let target_side = self.allowed_target_side_locked(
2535                    &st,
2536                    exclude,
2537                    ty,
2538                    st.reliable_return_routes
2539                        .get(&packet_id)
2540                        .map(|route| route.side),
2541                );
2542                if let Some(side) = target_side {
2543                    return Ok(RemoteSidePlan::Target(vec![side]));
2544                }
2545                return Ok(RemoteSidePlan::Target(Vec::new()));
2546            }
2547            let sides = self.eligible_side_ids_locked(&st, Some(exclude), Some(ty), false);
2548            Ok(RemoteSidePlan::Target(self.apply_route_selection_locked(
2549                &mut st,
2550                Some(exclude),
2551                sides,
2552                RouteSelectionOrigin::Flood,
2553            )))
2554        }
2555    }
2556
2557    #[inline]
2558    fn allowed_target_side_locked(
2559        &self,
2560        st: &RelayInner,
2561        exclude: RelaySideId,
2562        ty: crate::DataType,
2563        target_side: Option<RelaySideId>,
2564    ) -> Option<RelaySideId> {
2565        target_side.filter(|side| self.route_allowed_locked(st, Some(exclude), Some(ty), *side))
2566    }
2567
2568    fn filter_end_to_end_satisfied_sides_locked(
2569        &self,
2570        st: &RelayInner,
2571        data: &RelayItem,
2572        sides: Vec<RelaySideId>,
2573        eps: &[crate::DataEndpoint],
2574        ty: crate::DataType,
2575    ) -> TelemetryResult<Vec<RelaySideId>> {
2576        if !is_reliable_type(ty) || Self::reliable_control_target_packet_id(data)?.is_some() {
2577            return Ok(sides);
2578        }
2579        let packet_id = match data {
2580            RelayItem::Packet(pkt) => pkt.packet_id(),
2581            RelayItem::Packed(bytes) => match wire_format::packet_id_from_wire(bytes.as_ref()) {
2582                Ok(packet_id) => packet_id,
2583                Err(TelemetryError::Unpack("reliable control frame")) => return Ok(sides),
2584                Err(err) => return Err(err),
2585            },
2586        };
2587        let Some(acked) = st.end_to_end_acked_destinations.get(&packet_id) else {
2588            return Ok(sides);
2589        };
2590        let now_ms = self.clock.now_ms();
2591        let mut filtered = Vec::new();
2592        for side in sides {
2593            let Some(route) = st.discovery_routes.get(&side) else {
2594                filtered.push(side);
2595                continue;
2596            };
2597            let mut still_pending = false;
2598            let mut had_destination_board = false;
2599            for sender_state in route.announcers.values() {
2600                if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2601                    continue;
2602                }
2603                for board in sender_state.topology_boards.iter() {
2604                    if !self.is_end_to_end_destination_sender(&board.sender_id) {
2605                        continue;
2606                    }
2607                    had_destination_board = true;
2608                    let sender_hash = Self::sender_hash(&board.sender_id);
2609                    if acked.contains(&sender_hash) {
2610                        continue;
2611                    }
2612                    if eps
2613                        .iter()
2614                        .copied()
2615                        .any(|ep| board.reachable_endpoints.contains(&ep))
2616                    {
2617                        still_pending = true;
2618                        break;
2619                    }
2620                    // Keep forwarding while any discovered destination sender for this packet
2621                    // remains unacked, even if topology/schema metadata changed for new packets.
2622                    still_pending = true;
2623                    break;
2624                }
2625                if still_pending {
2626                    break;
2627                }
2628            }
2629            if still_pending || !had_destination_board {
2630                filtered.push(side);
2631            }
2632        }
2633        Ok(filtered)
2634    }
2635
2636    #[cfg(feature = "discovery")]
2637    fn side_has_multiple_announcers_locked(
2638        &self,
2639        st: &RelayInner,
2640        side: RelaySideId,
2641        now_ms: u64,
2642    ) -> bool {
2643        st.discovery_routes
2644            .get(&side)
2645            .map(|route| {
2646                route
2647                    .announcers
2648                    .values()
2649                    .filter(|sender| {
2650                        now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
2651                    })
2652                    .take(2)
2653                    .count()
2654                    > 1
2655            })
2656            .unwrap_or(false)
2657    }
2658
2659    #[cfg(not(feature = "discovery"))]
2660    fn side_has_multiple_announcers_locked(
2661        &self,
2662        _st: &RelayInner,
2663        _side: RelaySideId,
2664        _now_ms: u64,
2665    ) -> bool {
2666        false
2667    }
2668
2669    #[cfg(feature = "discovery")]
2670    fn sender_topology_board_mut<'a>(
2671        sender_state: &'a mut DiscoverySenderState,
2672        sender_id: &str,
2673    ) -> &'a mut TopologyBoardNode {
2674        if let Some(idx) = sender_state
2675            .topology_boards
2676            .iter()
2677            .position(|board| board.sender_id == sender_id)
2678        {
2679            return &mut sender_state.topology_boards[idx];
2680        }
2681        sender_state.topology_boards.push(TopologyBoardNode {
2682            sender_id: sender_id.to_string(),
2683            reachable_endpoints: Vec::new(),
2684            reachable_timesync_sources: Vec::new(),
2685            connections: Vec::new(),
2686        });
2687        sender_state
2688            .topology_boards
2689            .last_mut()
2690            .expect("board inserted above")
2691    }
2692
2693    #[cfg(feature = "discovery")]
2694    fn refresh_sender_topology_state(sender_state: &mut DiscoverySenderState) {
2695        discovery::normalize_topology_boards(&mut sender_state.topology_boards);
2696        let (mut reachable, mut reachable_timesync_sources) =
2697            discovery::summarize_topology_boards(&sender_state.topology_boards);
2698        reachable.extend(sender_state.advertised_reachable.iter().copied());
2699        reachable.sort_unstable();
2700        reachable.dedup();
2701        reachable_timesync_sources.extend(
2702            sender_state
2703                .advertised_reachable_timesync_sources
2704                .iter()
2705                .cloned(),
2706        );
2707        reachable_timesync_sources.sort_unstable();
2708        reachable_timesync_sources.dedup();
2709        sender_state.reachable = reachable;
2710        sender_state.reachable_timesync_sources = reachable_timesync_sources;
2711    }
2712
2713    #[cfg(feature = "discovery")]
2714    fn recompute_discovery_side_state(route: &mut DiscoverySideState) {
2715        let mut reachable = Vec::new();
2716        let mut reachable_timesync_sources = Vec::new();
2717        let mut last_seen_ms = 0u64;
2718        for sender in route.announcers.values() {
2719            reachable.extend(sender.reachable.iter().copied());
2720            reachable_timesync_sources.extend(sender.reachable_timesync_sources.iter().cloned());
2721            last_seen_ms = last_seen_ms.max(sender.last_seen_ms);
2722        }
2723        reachable.sort_unstable();
2724        reachable.dedup();
2725        reachable_timesync_sources.sort_unstable();
2726        reachable_timesync_sources.dedup();
2727        route.reachable = reachable;
2728        route.reachable_timesync_sources = reachable_timesync_sources;
2729        route.last_seen_ms = last_seen_ms;
2730    }
2731
2732    #[cfg(feature = "discovery")]
2733    fn local_discovery_topology_board(&self, st: &RelayInner, now_ms: u64) -> TopologyBoardNode {
2734        let mut connections = Vec::new();
2735        for route in st.discovery_routes.values() {
2736            if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2737                continue;
2738            }
2739            for (sender, sender_state) in route.announcers.iter() {
2740                if now_ms.saturating_sub(sender_state.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS {
2741                    connections.push(sender.clone());
2742                }
2743            }
2744        }
2745        connections.sort_unstable();
2746        connections.dedup();
2747        let sender = self.sender_arc();
2748        TopologyBoardNode {
2749            sender_id: sender.to_string(),
2750            reachable_endpoints: Vec::new(),
2751            reachable_timesync_sources: Vec::new(),
2752            connections,
2753        }
2754    }
2755
2756    #[cfg(feature = "discovery")]
2757    fn advertised_discovery_topology_for_link_locked(
2758        &self,
2759        st: &RelayInner,
2760        now_ms: u64,
2761        link_local_enabled: bool,
2762    ) -> Vec<TopologyBoardNode> {
2763        let mut boards = vec![self.local_discovery_topology_board(st, now_ms)];
2764        for route in st.discovery_routes.values() {
2765            if now_ms.saturating_sub(route.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2766                continue;
2767            }
2768            for (announcer, sender_state) in route.announcers.iter() {
2769                if now_ms.saturating_sub(sender_state.last_seen_ms) > DISCOVERY_ROUTE_TTL_MS {
2770                    continue;
2771                }
2772                let mut sender_boards = sender_state.topology_boards.clone();
2773                if sender_boards.is_empty() {
2774                    let sender = self.sender_arc();
2775                    sender_boards.push(TopologyBoardNode {
2776                        sender_id: announcer.clone(),
2777                        reachable_endpoints: sender_state.reachable.clone(),
2778                        reachable_timesync_sources: sender_state.reachable_timesync_sources.clone(),
2779                        connections: vec![sender.to_string()],
2780                    });
2781                } else if let Some(board) = sender_boards
2782                    .iter_mut()
2783                    .find(|board| board.sender_id == *announcer)
2784                {
2785                    board.connections.push(self.sender_arc().to_string());
2786                }
2787                if !link_local_enabled {
2788                    for board in sender_boards.iter_mut() {
2789                        board
2790                            .reachable_endpoints
2791                            .retain(|ep| !ep.is_link_local_only());
2792                    }
2793                }
2794                discovery::merge_topology_boards(&mut boards, &sender_boards);
2795            }
2796        }
2797        discovery::normalize_topology_boards(&mut boards);
2798        boards
2799    }
2800
2801    #[cfg(feature = "discovery")]
2802    fn note_discovery_topology_change_locked(st: &mut RelayInner, now_ms: u64) {
2803        st.discovery_cadence.on_topology_change(now_ms);
2804        for side_id in st
2805            .sides
2806            .iter()
2807            .enumerate()
2808            .filter_map(|(side_id, side)| side.as_ref().map(|_| side_id))
2809        {
2810            st.discovery_side_throttle
2811                .entry(side_id)
2812                .or_default()
2813                .pending_incremental = true;
2814            st.discovery_side_throttle
2815                .entry(side_id)
2816                .or_default()
2817                .incremental_retries_remaining = DISCOVERY_INCREMENTAL_RETRY_COUNT;
2818        }
2819        for transport in st.side_transport.values_mut() {
2820            transport.clear_header_templates();
2821        }
2822    }
2823
2824    #[cfg(feature = "discovery")]
2825    fn prune_discovery_routes_locked(st: &mut RelayInner, now_ms: u64) -> bool {
2826        let before = st.discovery_routes.clone();
2827        st.discovery_routes.retain(|_, route| {
2828            route.announcers.retain(|_, sender| {
2829                now_ms.saturating_sub(sender.last_seen_ms) <= DISCOVERY_ROUTE_TTL_MS
2830            });
2831            Self::recompute_discovery_side_state(route);
2832            !route.announcers.is_empty()
2833        });
2834        st.discovery_routes != before
2835    }
2836
2837    #[cfg(feature = "discovery")]
2838    fn reconcile_end_to_end_acked_destinations_locked(&self, st: &mut RelayInner) {
2839        let mut active_senders = BTreeSet::new();
2840        for route in st.discovery_routes.values() {
2841            for sender_state in route.announcers.values() {
2842                for board in sender_state.topology_boards.iter() {
2843                    if self.is_end_to_end_destination_sender(&board.sender_id) {
2844                        active_senders.insert(Self::sender_hash(&board.sender_id));
2845                    }
2846                }
2847            }
2848        }
2849        st.end_to_end_acked_destinations.retain(|_, acked| {
2850            acked.retain(|sender_hash| active_senders.contains(sender_hash));
2851            !acked.is_empty()
2852        });
2853    }
2854
2855    #[cfg(feature = "discovery")]
2856    fn advertised_discovery_endpoints_for_link_locked(
2857        &self,
2858        st: &RelayInner,
2859        now_ms: u64,
2860        link_local_enabled: bool,
2861    ) -> Vec<crate::DataEndpoint> {
2862        let (reachable_endpoints, _) = discovery::summarize_topology_boards(
2863            &self.advertised_discovery_topology_for_link_locked(st, now_ms, link_local_enabled),
2864        );
2865        reachable_endpoints
2866            .into_iter()
2867            .filter(|ep| {
2868                !discovery::is_discovery_endpoint(*ep)
2869                    && (link_local_enabled || !ep.is_link_local_only())
2870            })
2871            .collect()
2872    }
2873
2874    #[cfg(feature = "discovery")]
2875    fn advertised_discovery_timesync_sources_for_link_locked(
2876        &self,
2877        st: &RelayInner,
2878        now_ms: u64,
2879    ) -> Vec<String> {
2880        let (_, sources) = discovery::summarize_topology_boards(
2881            &self.advertised_discovery_topology_for_link_locked(st, now_ms, true),
2882        );
2883        sources
2884    }
2885
2886    #[cfg(feature = "discovery")]
2887    #[cfg(feature = "timesync")]
2888    fn preferred_timesync_route_source(
2889        &self,
2890        data: &RelayItem,
2891        ty: crate::DataType,
2892    ) -> TelemetryResult<Option<String>> {
2893        if !matches!(
2894            ty,
2895            crate::DataType::TimeSyncAnnounce | crate::DataType::TimeSyncResponse
2896        ) {
2897            return Ok(None);
2898        }
2899
2900        let sender = match data {
2901            RelayItem::Packet(pkt) => pkt.sender().to_owned(),
2902            RelayItem::Packed(bytes) => {
2903                if wire_format::peek_frame_info(bytes.as_ref())
2904                    .ok()
2905                    .is_some_and(|frame| frame.ack_only())
2906                {
2907                    return Ok(None);
2908                }
2909                wire_format::unpack_packet(bytes.as_ref())?
2910                    .sender()
2911                    .to_owned()
2912            }
2913        };
2914        Ok(Some(sender))
2915    }
2916
2917    #[cfg(feature = "discovery")]
2918    #[inline]
2919    fn side_is_slow_control_link_locked(
2920        st: &RelayInner,
2921        side_id: RelaySideId,
2922        now_ms: u64,
2923    ) -> bool {
2924        st.adaptive_route_stats.get(&side_id).is_some_and(|stats| {
2925            let recent_slow = stats.last_slow_observed_ms > 0
2926                && now_ms.saturating_sub(stats.last_slow_observed_ms)
2927                    <= DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS;
2928            stats.sample_count > 0
2929                && ((stats.estimated_bandwidth_bps > 0
2930                    && stats.estimated_bandwidth_bps <= CONTROL_SLOW_LINK_CAPACITY_BPS)
2931                    || recent_slow)
2932        })
2933    }
2934
2935    #[cfg(feature = "discovery")]
2936    fn discovery_level_for_side_locked(
2937        st: &mut RelayInner,
2938        side_id: RelaySideId,
2939        now_ms: u64,
2940    ) -> Option<DiscoveryAdvertiseLevel> {
2941        let slow = Self::side_is_slow_control_link_locked(st, side_id, now_ms);
2942        let throttle = st.discovery_side_throttle.entry(side_id).or_default();
2943        if !throttle.has_sent_full || now_ms >= throttle.next_full_ms {
2944            throttle.has_sent_full = true;
2945            throttle.pending_incremental = false;
2946            throttle.incremental_retries_remaining = 0;
2947            throttle.next_full_ms = now_ms.saturating_add(DISCOVERY_SLOW_LINK_FULL_INTERVAL_MS);
2948            throttle.next_ping_ms = now_ms.saturating_add(DISCOVERY_SLOW_LINK_PING_INTERVAL_MS);
2949            return Some(DiscoveryAdvertiseLevel::Full);
2950        }
2951        if throttle.pending_incremental {
2952            throttle.incremental_retries_remaining =
2953                throttle.incremental_retries_remaining.saturating_sub(1);
2954            throttle.pending_incremental = throttle.incremental_retries_remaining > 0;
2955            return Some(DiscoveryAdvertiseLevel::Incremental);
2956        }
2957        if slow && now_ms >= throttle.next_ping_ms {
2958            throttle.next_ping_ms = now_ms.saturating_add(DISCOVERY_SLOW_LINK_PING_INTERVAL_MS);
2959            return Some(DiscoveryAdvertiseLevel::MinimalPing);
2960        }
2961        if slow {
2962            None
2963        } else {
2964            Some(DiscoveryAdvertiseLevel::Incremental)
2965        }
2966    }
2967
2968    #[cfg(all(feature = "discovery", feature = "timesync"))]
2969    #[inline]
2970    fn is_timesync_type(ty: crate::DataType) -> bool {
2971        matches!(
2972            ty,
2973            crate::DataType::TimeSyncAnnounce
2974                | crate::DataType::TimeSyncRequest
2975                | crate::DataType::TimeSyncResponse
2976        )
2977    }
2978
2979    #[cfg(all(feature = "discovery", feature = "timesync"))]
2980    fn timesync_allowed_for_side_locked(
2981        st: &mut RelayInner,
2982        side_id: RelaySideId,
2983        ty: crate::DataType,
2984        now_ms: u64,
2985    ) -> bool {
2986        if !Self::is_timesync_type(ty) {
2987            return true;
2988        }
2989        if !Self::side_is_slow_control_link_locked(st, side_id, now_ms) {
2990            st.timesync_side_throttle.remove(&side_id);
2991            return true;
2992        }
2993
2994        let throttle = st.timesync_side_throttle.entry(side_id).or_default();
2995        if now_ms >= throttle.next_allowed_ms {
2996            throttle.next_allowed_ms = now_ms.saturating_add(TIMESYNC_SLOW_LINK_MIN_INTERVAL_MS);
2997            return true;
2998        }
2999        false
3000    }
3001
3002    #[cfg(all(feature = "discovery", feature = "timesync"))]
3003    fn filter_timesync_sides_locked(
3004        st: &mut RelayInner,
3005        ty: crate::DataType,
3006        now_ms: u64,
3007        sides: Vec<RelaySideId>,
3008    ) -> Vec<RelaySideId> {
3009        sides
3010            .into_iter()
3011            .filter(|side| Self::timesync_allowed_for_side_locked(st, *side, ty, now_ms))
3012            .collect()
3013    }
3014
3015    #[cfg(feature = "discovery")]
3016    fn queue_discovery_announce(
3017        &self,
3018        include_schema: bool,
3019        skip_in_flight_sides: bool,
3020    ) -> TelemetryResult<()> {
3021        #[cfg(not(feature = "std"))]
3022        let _ = include_schema;
3023        let now_ms = self.clock.now_ms();
3024        let per_side = {
3025            let mut st = self.state.lock();
3026            if Self::prune_discovery_routes_locked(&mut st, now_ms) {
3027                self.reconcile_end_to_end_acked_destinations_locked(&mut st);
3028                Self::note_discovery_topology_change_locked(&mut st, now_ms);
3029            }
3030            st.fit_discovery_budget();
3031            if !st.sides.iter().any(|side| side.is_some()) {
3032                return Ok(());
3033            }
3034            if include_schema {
3035                for throttle in st.discovery_side_throttle.values_mut() {
3036                    throttle.has_sent_full = false;
3037                }
3038            }
3039            st.discovery_cadence.on_announce_sent(now_ms);
3040            let side_entries = st
3041                .sides
3042                .iter()
3043                .enumerate()
3044                .filter_map(|(side_id, side)| {
3045                    side.as_ref()
3046                        .map(|side| (side_id, side.opts.link_local_enabled, side.opts))
3047                })
3048                .collect::<Vec<_>>();
3049            let mut per_side = Vec::new();
3050            for (side_id, link_local_enabled, opts) in side_entries {
3051                if skip_in_flight_sides
3052                    && st.reliable_tx.iter().any(|((pending_side, ty), tx_state)| {
3053                        *pending_side == side_id
3054                            && !tx_state.sent.is_empty()
3055                            && crate::DataType::try_from_u32(*ty)
3056                                .is_some_and(discovery::is_discovery_type)
3057                    })
3058                {
3059                    continue;
3060                }
3061                if !self.route_allowed_locked(
3062                    &st,
3063                    None,
3064                    Some(crate::DataType::DiscoveryAnnounce),
3065                    side_id,
3066                ) {
3067                    continue;
3068                }
3069                let Some(level) = Self::discovery_level_for_side_locked(&mut st, side_id, now_ms)
3070                else {
3071                    continue;
3072                };
3073                let capabilities = opts.link_capabilities();
3074                if level == DiscoveryAdvertiseLevel::MinimalPing {
3075                    per_side.push((
3076                        side_id,
3077                        level,
3078                        Vec::new(),
3079                        Vec::new(),
3080                        Vec::new(),
3081                        Vec::new(),
3082                        capabilities,
3083                    ));
3084                    continue;
3085                }
3086                let endpoints = self.advertised_discovery_endpoints_for_link_locked(
3087                    &st,
3088                    now_ms,
3089                    link_local_enabled,
3090                );
3091                let timesync_sources =
3092                    self.advertised_discovery_timesync_sources_for_link_locked(&st, now_ms);
3093                let current_topology = self.advertised_discovery_topology_for_link_locked(
3094                    &st,
3095                    now_ms,
3096                    link_local_enabled,
3097                );
3098                let (topology, removed_topology) = match level {
3099                    DiscoveryAdvertiseLevel::Full => (current_topology.clone(), Vec::new()),
3100                    DiscoveryAdvertiseLevel::Incremental => {
3101                        let previous = &st
3102                            .discovery_side_throttle
3103                            .get(&side_id)
3104                            .expect("discovery throttle exists")
3105                            .last_topology;
3106                        discovery::topology_delta(previous, &current_topology)
3107                    }
3108                    DiscoveryAdvertiseLevel::MinimalPing => (Vec::new(), Vec::new()),
3109                };
3110                let throttle = st
3111                    .discovery_side_throttle
3112                    .get_mut(&side_id)
3113                    .expect("discovery throttle exists");
3114                if level == DiscoveryAdvertiseLevel::Full
3115                    || (level == DiscoveryAdvertiseLevel::Incremental
3116                        && throttle.incremental_retries_remaining == 0)
3117                {
3118                    throttle.last_topology = current_topology;
3119                }
3120                per_side.push((
3121                    side_id,
3122                    level,
3123                    endpoints,
3124                    timesync_sources,
3125                    topology,
3126                    removed_topology,
3127                    capabilities,
3128                ));
3129            }
3130            per_side
3131        };
3132        let mut st = self.state.lock();
3133        for (dst, level, endpoints, timesync_sources, topology, removed_topology, capabilities) in
3134            per_side
3135        {
3136            let sender = self.sender_arc();
3137            if include_schema && level == DiscoveryAdvertiseLevel::Full {
3138                let pkt = discovery::build_discovery_schema(sender.as_ref(), now_ms)?;
3139                let data = RelayItem::Packet(Arc::new(pkt));
3140                let priority = Self::relay_item_priority(&data)?;
3141                st.push_tx(RelayTxItem {
3142                    src: None,
3143                    dst,
3144                    data,
3145                    priority,
3146                })?;
3147            }
3148            if level == DiscoveryAdvertiseLevel::Full {
3149                let pkt = discovery::build_discovery_link_capabilities(
3150                    sender.as_ref(),
3151                    now_ms,
3152                    capabilities,
3153                )?;
3154                let data = RelayItem::Packet(Arc::new(pkt));
3155                let priority = Self::relay_item_priority(&data)?;
3156                st.push_tx(RelayTxItem {
3157                    src: None,
3158                    dst,
3159                    data,
3160                    priority,
3161                })?;
3162            }
3163            if level != DiscoveryAdvertiseLevel::Full || !endpoints.is_empty() {
3164                let pkt = discovery::build_discovery_announce(
3165                    sender.as_ref(),
3166                    now_ms,
3167                    endpoints.as_slice(),
3168                )?;
3169                let data = RelayItem::Packet(Arc::new(pkt));
3170                let priority = Self::relay_item_priority(&data)?;
3171                st.push_tx(RelayTxItem {
3172                    src: None,
3173                    dst,
3174                    data,
3175                    priority,
3176                })?;
3177            }
3178            if level != DiscoveryAdvertiseLevel::MinimalPing && !timesync_sources.is_empty() {
3179                let pkt = discovery::build_discovery_timesync_sources(
3180                    sender.as_ref(),
3181                    now_ms,
3182                    timesync_sources.as_slice(),
3183                )?;
3184                let data = RelayItem::Packet(Arc::new(pkt));
3185                let priority = Self::relay_item_priority(&data)?;
3186                st.push_tx(RelayTxItem {
3187                    src: None,
3188                    dst,
3189                    data,
3190                    priority,
3191                })?;
3192            }
3193            if level != DiscoveryAdvertiseLevel::MinimalPing
3194                && (!topology.is_empty() || !removed_topology.is_empty())
3195            {
3196                let pkt = if level == DiscoveryAdvertiseLevel::Full {
3197                    discovery::build_discovery_topology(sender.as_ref(), now_ms, &topology)?
3198                } else {
3199                    discovery::build_discovery_topology_delta(
3200                        sender.as_ref(),
3201                        now_ms,
3202                        &topology,
3203                        &removed_topology,
3204                    )?
3205                };
3206                let data = RelayItem::Packet(Arc::new(pkt));
3207                let priority = Self::relay_item_priority(&data)?;
3208                st.push_tx(RelayTxItem {
3209                    src: None,
3210                    dst,
3211                    data,
3212                    priority,
3213                })?;
3214            }
3215        }
3216        Ok(())
3217    }
3218
3219    #[cfg(feature = "discovery")]
3220    fn poll_discovery_announce(&self) -> TelemetryResult<bool> {
3221        let now_ms = self.clock.now_ms();
3222        let due = {
3223            let mut st = self.state.lock();
3224            let removed = Self::prune_discovery_routes_locked(&mut st, now_ms);
3225            if removed {
3226                self.reconcile_end_to_end_acked_destinations_locked(&mut st);
3227                Self::note_discovery_topology_change_locked(&mut st, now_ms);
3228            }
3229            st.fit_discovery_budget();
3230            let has_any = st.sides.iter().enumerate().any(|(side_id, side)| {
3231                let Some(side) = side.as_ref() else {
3232                    return false;
3233                };
3234                if !self.route_allowed_locked(
3235                    &st,
3236                    None,
3237                    Some(crate::DataType::DiscoveryAnnounce),
3238                    side_id,
3239                ) {
3240                    return false;
3241                }
3242                let _ = side;
3243                !st.reliable_tx.iter().any(|((pending_side, ty), tx_state)| {
3244                    *pending_side == side_id
3245                        && !tx_state.sent.is_empty()
3246                        && crate::DataType::try_from_u32(*ty)
3247                            .is_some_and(discovery::is_discovery_type)
3248                })
3249            });
3250            if !st.sides.iter().any(|side| side.is_some()) || !has_any {
3251                return Ok(false);
3252            }
3253            st.discovery_cadence.due(now_ms)
3254        };
3255        if !due {
3256            return Ok(false);
3257        }
3258        // Keep periodic discovery lightweight so a hosted relay cannot fill a
3259        // constrained link with repeated schema snapshots.
3260        self.queue_discovery_announce(false, true)?;
3261        Ok(true)
3262    }
3263
3264    #[cfg(feature = "discovery")]
3265    fn learn_discovery_item(&self, src: RelaySideId, data: &RelayItem) -> TelemetryResult<()> {
3266        let pkt = match data {
3267            RelayItem::Packet(pkt) => {
3268                if !discovery::is_discovery_type(pkt.data_type()) {
3269                    return Ok(());
3270                }
3271                pkt.as_ref().clone()
3272            }
3273            RelayItem::Packed(bytes) => {
3274                let env = wire_format::peek_envelope(bytes.as_ref())?;
3275                if !discovery::is_discovery_type(env.ty) {
3276                    return Ok(());
3277                }
3278                if wire_format::peek_frame_info(bytes.as_ref())
3279                    .ok()
3280                    .is_some_and(|frame| frame.ack_only())
3281                {
3282                    return Ok(());
3283                }
3284                wire_format::unpack_packet(bytes.as_ref())?
3285            }
3286        };
3287
3288        let now_ms = self.clock.now_ms();
3289        if pkt.data_type() == crate::DataType::DiscoverySchema {
3290            let snapshot = discovery::decode_discovery_schema(&pkt)?;
3291            let incoming_cost = crate::config::owned_schema_byte_cost(&snapshot);
3292            let mut st = self.state.lock();
3293            st.make_shared_queue_room(incoming_cost, RelayQueueKind::Discovery)?;
3294            let budget = st.memory.max_queue_budget;
3295            drop(st);
3296            let report = crate::config::merge_owned_schema_snapshot_with_budget(snapshot, budget)?;
3297            if report.changed() {
3298                let mut st = self.state.lock();
3299                st.fit_discovery_budget();
3300                Self::note_discovery_topology_change_locked(&mut st, now_ms);
3301            }
3302            return Ok(());
3303        }
3304        if pkt.data_type() == crate::DataType::DiscoveryLinkCapabilities {
3305            let _ = discovery::decode_discovery_link_capabilities(&pkt)?;
3306            return Ok(());
3307        }
3308        let mut st = self.state.lock();
3309        if pkt.data_type() == crate::DataType::DiscoveryLeave {
3310            let leaving = pkt.sender();
3311            let before = st.discovery_routes.clone();
3312            for route in st.discovery_routes.values_mut() {
3313                route.announcers.remove(leaving);
3314                for sender_state in route.announcers.values_mut() {
3315                    sender_state
3316                        .topology_boards
3317                        .retain(|board| board.sender_id != leaving);
3318                    for board in sender_state.topology_boards.iter_mut() {
3319                        board.connections.retain(|peer| peer != leaving);
3320                    }
3321                    Self::refresh_sender_topology_state(sender_state);
3322                }
3323                Self::recompute_discovery_side_state(route);
3324            }
3325            st.discovery_routes
3326                .retain(|_, route| !route.announcers.is_empty());
3327            if st.discovery_routes != before {
3328                Self::note_discovery_topology_change_locked(&mut st, now_ms);
3329            }
3330            let _ = Self::prune_discovery_routes_locked(&mut st, now_ms);
3331            self.reconcile_end_to_end_acked_destinations_locked(&mut st);
3332            return Ok(());
3333        }
3334        let address_ad = if pkt.data_type() == crate::DataType::DiscoveryAddress {
3335            Some(discovery::decode_discovery_address(&pkt)?)
3336        } else {
3337            None
3338        };
3339        let mut topology_ad = if pkt.data_type() == crate::DataType::DiscoveryTopology {
3340            Some(discovery::decode_discovery_topology_update(&pkt)?)
3341        } else {
3342            None
3343        };
3344        let announcer_id = if let Some(ad) = address_ad.as_ref() {
3345            ad.hostname.clone()
3346        } else {
3347            let canonical = Self::canonical_sender_locked(&st, pkt.sender());
3348            if canonical != pkt.sender() {
3349                canonical
3350            } else if let Some(address) = pkt
3351                .sender()
3352                .strip_prefix("@addr:")
3353                .and_then(|value| value.parse::<u32>().ok())
3354            {
3355                topology_ad
3356                    .as_ref()
3357                    .and_then(|update| {
3358                        update
3359                            .boards
3360                            .iter()
3361                            .find(|board| sender_address_u32(&board.sender_id) == address)
3362                    })
3363                    .map(|board| board.sender_id.clone())
3364                    .unwrap_or(canonical)
3365            } else {
3366                canonical
3367            }
3368        };
3369        let mut route = st.discovery_routes.get(&src).cloned().unwrap_or_default();
3370        if pkt.sender() != announcer_id {
3371            route.announcers.remove(pkt.sender());
3372        }
3373        let side_link_local_enabled = st
3374            .sides
3375            .get(src)
3376            .and_then(|entry| entry.as_ref())
3377            .map(|side_ref| side_ref.opts.link_local_enabled)
3378            .unwrap_or(false);
3379        let mut sender_state = route
3380            .announcers
3381            .get(&announcer_id)
3382            .cloned()
3383            .unwrap_or_default();
3384        let changed = match pkt.data_type() {
3385            crate::DataType::DiscoveryAddress => {
3386                let ad = address_ad.expect("decoded above");
3387                let advertised_address = ad.address;
3388                let mut reachable = ad.reachable_endpoints;
3389                if !side_link_local_enabled {
3390                    reachable.retain(|ep| !ep.is_link_local_only());
3391                }
3392                // This packet is an aggregate route summary, not a claim that
3393                // the announcing bridge owns every endpoint behind it.
3394                let changed = sender_state.advertised_address != Some(advertised_address)
3395                    || sender_state.advertised_reachable != reachable
3396                    || sender_state.advertised_reachable_timesync_sources
3397                        != ad.reachable_timesync_sources;
3398                sender_state.advertised_address = Some(advertised_address);
3399                sender_state.advertised_reachable = reachable;
3400                sender_state.advertised_reachable_timesync_sources = ad.reachable_timesync_sources;
3401                Self::refresh_sender_topology_state(&mut sender_state);
3402                changed
3403            }
3404            crate::DataType::DiscoveryAnnounce => {
3405                let mut reachable = discovery::decode_discovery_announce(&pkt)?;
3406                if !side_link_local_enabled {
3407                    reachable.retain(|ep| !ep.is_link_local_only());
3408                }
3409                let board = Self::sender_topology_board_mut(&mut sender_state, &announcer_id);
3410                let changed = board.reachable_endpoints != reachable;
3411                board.reachable_endpoints = reachable;
3412                Self::refresh_sender_topology_state(&mut sender_state);
3413                changed
3414            }
3415            crate::DataType::DiscoveryTimeSyncSources => {
3416                let sources = discovery::decode_discovery_timesync_sources(&pkt)?;
3417                let board = Self::sender_topology_board_mut(&mut sender_state, &announcer_id);
3418                let changed = board.reachable_timesync_sources != sources;
3419                board.reachable_timesync_sources = sources;
3420                Self::refresh_sender_topology_state(&mut sender_state);
3421                changed
3422            }
3423            crate::DataType::DiscoveryTopology => {
3424                let mut update = topology_ad
3425                    .take()
3426                    .expect("topology packet was decoded before route selection");
3427                for board in update.boards.iter_mut() {
3428                    board.sender_id = Self::canonical_sender_locked(&st, &board.sender_id);
3429                    for peer in board.connections.iter_mut() {
3430                        *peer = Self::canonical_sender_locked(&st, peer);
3431                    }
3432                }
3433                if !side_link_local_enabled {
3434                    for board in update.boards.iter_mut() {
3435                        board
3436                            .reachable_endpoints
3437                            .retain(|ep| !ep.is_link_local_only());
3438                    }
3439                }
3440                let before = sender_state.topology_boards.clone();
3441                if update.incremental {
3442                    sender_state.topology_boards.retain(|board| {
3443                        !update
3444                            .removed
3445                            .iter()
3446                            .any(|removed| removed == &board.sender_id)
3447                            && !update
3448                                .boards
3449                                .iter()
3450                                .any(|replacement| replacement.sender_id == board.sender_id)
3451                    });
3452                    discovery::merge_topology_boards(
3453                        &mut sender_state.topology_boards,
3454                        &update.boards,
3455                    );
3456                } else {
3457                    sender_state.topology_boards = update.boards;
3458                }
3459                let changed = sender_state.topology_boards != before;
3460                Self::refresh_sender_topology_state(&mut sender_state);
3461                changed
3462            }
3463            crate::DataType::DiscoverySchema => false,
3464            _ => false,
3465        };
3466        sender_state.last_seen_ms = now_ms;
3467        route.announcers.insert(announcer_id, sender_state);
3468        Self::recompute_discovery_side_state(&mut route);
3469        st.discovery_routes.insert(src, route);
3470        st.fit_discovery_budget();
3471        if changed {
3472            Self::note_discovery_topology_change_locked(&mut st, now_ms);
3473        }
3474        let _ = Self::prune_discovery_routes_locked(&mut st, now_ms);
3475        self.reconcile_end_to_end_acked_destinations_locked(&mut st);
3476        Ok(())
3477    }
3478
3479    #[cfg(not(feature = "discovery"))]
3480    fn learn_discovery_item(&self, _src: RelaySideId, _data: &RelayItem) -> TelemetryResult<()> {
3481        Ok(())
3482    }
3483
3484    #[cfg(not(feature = "discovery"))]
3485    fn queue_discovery_announce(&self) -> TelemetryResult<()> {
3486        Ok(())
3487    }
3488
3489    #[cfg(not(feature = "discovery"))]
3490    fn poll_discovery_announce(&self) -> TelemetryResult<bool> {
3491        Ok(false)
3492    }
3493
3494    fn process_reliable_timeouts(&self) -> TelemetryResult<()> {
3495        let now = self.clock.now_ms();
3496        let mut requeue: Vec<(RelaySideId, crate::DataType, u32)> = Vec::new();
3497
3498        {
3499            let mut st = self.state.lock();
3500            if st.reliable_tx.is_empty() {
3501                return Ok(());
3502            }
3503
3504            for ((side, ty_u32), tx_state) in st.reliable_tx.iter_mut() {
3505                let Some(ty) = crate::DataType::try_from_u32(*ty_u32) else {
3506                    continue;
3507                };
3508                let sent_order: Vec<u32> = tx_state.sent_order.iter().copied().collect();
3509                for seq in sent_order {
3510                    let Some(sent) = tx_state.sent.get_mut(&seq) else {
3511                        continue;
3512                    };
3513                    if sent.queued
3514                        || now.wrapping_sub(sent.last_send_ms) < runtime_reliable_retransmit_ms()
3515                    {
3516                        continue;
3517                    }
3518                    if sent.partial_acked {
3519                        // Do not let a lost packet-request pin a partially
3520                        // acknowledged entry forever. Allow one interval for
3521                        // the explicit request, then return to bounded replay.
3522                        sent.partial_acked = false;
3523                        sent.last_send_ms = now;
3524                        continue;
3525                    }
3526                    if sent.retries >= runtime_reliable_max_retries() {
3527                        tx_state.sent.remove(&seq);
3528                        tx_state.sent_order.retain(|existing| *existing != seq);
3529                        continue;
3530                    }
3531                    sent.retries += 1;
3532                    requeue.push((*side, ty, seq));
3533                }
3534            }
3535        }
3536
3537        for (side, ty, seq) in requeue {
3538            self.queue_reliable_retransmit(side, ty, seq)?;
3539        }
3540
3541        Ok(())
3542    }
3543
3544    /// Compute a de-dupe hash for a QueueItem.
3545    /// Uses packet ID for Packet items, and attempts to extract packet ID from
3546    /// packed bytes. If extraction fails, hashes raw bytes as a fallback.
3547    fn get_hash(item: &RelayRxItem) -> u64 {
3548        match &item.data {
3549            RelayItem::Packet(pkt) => pkt.packet_id(),
3550            RelayItem::Packed(bytes) => {
3551                let reliable_seq = wire_format::peek_frame_info(bytes.as_ref())
3552                    .ok()
3553                    .and_then(|frame| frame.reliable)
3554                    .and_then(|hdr| {
3555                        if (hdr.flags & wire_format::RELIABLE_FLAG_ACK_ONLY) != 0 {
3556                            None
3557                        } else {
3558                            Some(hdr.seq)
3559                        }
3560                    });
3561
3562                match wire_format::packet_id_from_wire(bytes.as_ref()) {
3563                    Ok(id) => {
3564                        if let Some(seq) = reliable_seq {
3565                            hash_bytes_u64(id, &seq.to_le_bytes())
3566                        } else {
3567                            id
3568                        }
3569                    }
3570                    Err(_e) => {
3571                        // Fallback: if bytes are malformed (or compression feature mismatch),
3572                        // hash raw bytes so we can still dedupe identical network duplicates.
3573                        let h: u64 = 0x9E37_79B9_7F4A_7C15;
3574                        hash_bytes_u64(h, bytes.as_ref())
3575                    }
3576                }
3577            }
3578        }
3579    }
3580
3581    /// Compute a dedupe ID for an incoming RelayRxItem.
3582    /// Note: we intentionally do *not* include `src` so that the same
3583    /// packet coming from multiple sides is only processed once.
3584    fn is_duplicate_pkt(&self, item: &RelayRxItem) -> TelemetryResult<bool> {
3585        let id = Self::get_hash(item);
3586
3587        let mut st = self.state.lock();
3588        if st.recent_rx.contains(&id) {
3589            Ok(true)
3590        } else {
3591            st.push_recent_rx(id)?;
3592            Ok(false)
3593        }
3594    }
3595
3596    fn should_forward_duplicate_reliable_item(&self, item: &RelayRxItem) -> TelemetryResult<bool> {
3597        let (_, ty) = self.item_route_info(&item.data)?;
3598        if !is_reliable_type(ty)
3599            || matches!(
3600                ty,
3601                crate::DataType::ReliableAck
3602                    | crate::DataType::ReliablePartialAck
3603                    | crate::DataType::ReliablePacketRequest
3604            )
3605        {
3606            return Ok(false);
3607        }
3608
3609        let RemoteSidePlan::Target(sides) = self.remote_side_plan(&item.data, item.src)?;
3610        let st = self.state.lock();
3611        let now_ms = self.clock.now_ms();
3612        Ok(sides
3613            .into_iter()
3614            .any(|side| self.side_has_multiple_announcers_locked(&st, side, now_ms)))
3615    }
3616
3617    /// Register a side whose TX callback consumes packed packet bytes.
3618    ///
3619    /// Returns the side id later used for ingress APIs such as `rx_packed_from_side`.
3620    /// The default options disable the relay's per-link reliable framing on this side.
3621    pub fn add_side_packed<N, F>(&self, name: N, tx: F) -> RelaySideId
3622    where
3623        N: AsRef<str>,
3624        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
3625    {
3626        self.add_side_packed_with_options(name, tx, RelaySideOptions::default())
3627    }
3628
3629    /// Register a packed side with bounded-frame transport enabled.
3630    ///
3631    /// `max_frame_bytes == 0` leaves frames unbounded.
3632    pub fn add_side_packed_small_packets<N, F>(
3633        &self,
3634        name: N,
3635        tx: F,
3636        max_frame_bytes: usize,
3637    ) -> RelaySideId
3638    where
3639        N: AsRef<str>,
3640        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
3641    {
3642        self.add_side_packed_with_options(
3643            name,
3644            tx,
3645            RelaySideOptions::default().with_small_packet_transport(max_frame_bytes),
3646        )
3647    }
3648
3649    /// Register a packed-output side with explicit side options.
3650    ///
3651    /// `opts.reliable_enabled` enables relay-managed per-hop ACK/retransmit behavior on this side.
3652    /// `opts.link_local_enabled` gates link-local-only forwarding and discovery use of this side.
3653    /// `ingress_enabled` and `egress_enabled` set the initial directional policy.
3654    pub fn add_side_packed_with_options<N, F>(
3655        &self,
3656        name: N,
3657        tx: F,
3658        opts: RelaySideOptions,
3659    ) -> RelaySideId
3660    where
3661        N: AsRef<str>,
3662        F: Fn(&[u8]) -> TelemetryResult<()> + Send + Sync + 'static,
3663    {
3664        let mut st = self.state.lock();
3665        let side = Some(RelaySide {
3666            name: Arc::from(name.as_ref()),
3667            tx_handler: RelayTxHandlerFn::Packed(Arc::new(tx)),
3668            opts,
3669        });
3670        let id = if let Some(id) = st.sides.iter().position(Option::is_none) {
3671            st.sides[id] = side;
3672            id
3673        } else {
3674            let id = st.sides.len();
3675            st.sides.push(side);
3676            id
3677        };
3678        st.side_runtime_stats
3679            .insert(id, SideRuntimeStatsInner::default());
3680        st.side_transport.insert(id, SideTransportState::default());
3681        #[cfg(feature = "discovery")]
3682        Self::note_discovery_topology_change_locked(&mut st, self.clock.now_ms());
3683        id
3684    }
3685
3686    /// Register a side whose TX callback receives decoded [`Packet`] values.
3687    ///
3688    /// Packet-output sides do not preserve the relay's packed reliable hop framing, so use a
3689    /// packed side when this hop should participate in relay-managed per-link reliability.
3690    pub fn add_side_packet<N, F>(&self, name: N, tx: F) -> RelaySideId
3691    where
3692        N: AsRef<str>,
3693        F: Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static,
3694    {
3695        self.add_side_packet_with_options(name, tx, RelaySideOptions::default())
3696    }
3697
3698    /// Register a packet-output side with explicit side options.
3699    pub fn add_side_packet_with_options<N, F>(
3700        &self,
3701        name: N,
3702        tx: F,
3703        opts: RelaySideOptions,
3704    ) -> RelaySideId
3705    where
3706        N: AsRef<str>,
3707        F: Fn(&Packet) -> TelemetryResult<()> + Send + Sync + 'static,
3708    {
3709        let mut st = self.state.lock();
3710        let side = Some(RelaySide {
3711            name: Arc::from(name.as_ref()),
3712            tx_handler: RelayTxHandlerFn::Packet(Arc::new(tx)),
3713            opts,
3714        });
3715        let id = if let Some(id) = st.sides.iter().position(Option::is_none) {
3716            st.sides[id] = side;
3717            id
3718        } else {
3719            let id = st.sides.len();
3720            st.sides.push(side);
3721            id
3722        };
3723        st.side_runtime_stats
3724            .insert(id, SideRuntimeStatsInner::default());
3725        st.side_transport.insert(id, SideTransportState::default());
3726        #[cfg(feature = "discovery")]
3727        Self::note_discovery_topology_change_locked(&mut st, self.clock.now_ms());
3728        id
3729    }
3730
3731    /// Remove a side while keeping existing side IDs stable.
3732    ///
3733    /// `side` must be an id returned by one of the `add_side_*` calls. Remaining side ids are not
3734    /// renumbered.
3735    pub fn remove_side(&self, side: RelaySideId) -> TelemetryResult<()> {
3736        let now_ms = self.clock.now_ms();
3737        let mut st = self.state.lock();
3738        let slot = st.sides.get_mut(side).ok_or(TelemetryError::BadArg)?;
3739        if slot.is_none() {
3740            return Err(TelemetryError::BadArg);
3741        }
3742        *slot = None;
3743        while st.sides.last().is_some_and(Option::is_none) {
3744            st.sides.pop();
3745        }
3746        // Preserve capacity for link churn. It is bounded by the maximum number
3747        // of sides seen by this relay and is reclaimed when the relay drops.
3748        st.route_overrides
3749            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
3750        st.typed_route_overrides
3751            .retain(|(src_side, _, dst_side), _| *src_side != Some(side) && *dst_side != side);
3752        st.route_weights
3753            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
3754        st.route_priorities
3755            .retain(|(src_side, dst_side), _| *src_side != Some(side) && *dst_side != side);
3756        st.source_route_modes.remove(&Some(side));
3757        st.route_selection_cursors.remove(&Some(side));
3758        st.adaptive_route_stats.remove(&side);
3759        #[cfg(feature = "discovery")]
3760        st.discovery_side_throttle.remove(&side);
3761        #[cfg(all(feature = "discovery", feature = "timesync"))]
3762        st.timesync_side_throttle.remove(&side);
3763        st.side_runtime_stats.remove(&side);
3764        st.reliable_return_routes
3765            .retain(|_, route| route.side != side);
3766        st.rx_queue.retain(|queued| queued.src != side);
3767        st.tx_queue
3768            .retain(|queued| queued.dst != side && queued.src != Some(side));
3769        st.replay_queue.retain(|queued| queued.dst != side);
3770        st.reliable_tx.retain(|(side_id, _), _| *side_id != side);
3771        st.reliable_rx.retain(|(side_id, _), _| *side_id != side);
3772        #[cfg(feature = "discovery")]
3773        {
3774            st.discovery_routes.remove(&side);
3775            Self::note_discovery_topology_change_locked(&mut st, now_ms);
3776        }
3777        Ok(())
3778    }
3779
3780    /// Enable or disable ingress processing for a registered side.
3781    pub fn set_side_ingress_enabled(
3782        &self,
3783        side: RelaySideId,
3784        enabled: bool,
3785    ) -> TelemetryResult<()> {
3786        let now_ms = self.clock.now_ms();
3787        let mut st = self.state.lock();
3788        let side_ref = st
3789            .sides
3790            .get_mut(side)
3791            .and_then(|side| side.as_mut())
3792            .ok_or(TelemetryError::BadArg)?;
3793        side_ref.opts.ingress_enabled = enabled;
3794        #[cfg(feature = "discovery")]
3795        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3796        Ok(())
3797    }
3798
3799    /// Enable or disable egress toward a registered side.
3800    pub fn set_side_egress_enabled(&self, side: RelaySideId, enabled: bool) -> TelemetryResult<()> {
3801        let now_ms = self.clock.now_ms();
3802        let mut st = self.state.lock();
3803        let side_ref = st
3804            .sides
3805            .get_mut(side)
3806            .and_then(|side| side.as_mut())
3807            .ok_or(TelemetryError::BadArg)?;
3808        side_ref.opts.egress_enabled = enabled;
3809        if !enabled {
3810            st.tx_queue.retain(|queued| queued.dst != side);
3811            st.replay_queue.retain(|queued| queued.dst != side);
3812        }
3813        #[cfg(feature = "discovery")]
3814        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3815        Ok(())
3816    }
3817
3818    /// Set the route-selection policy for traffic originating from `src`.
3819    ///
3820    /// `src == None` targets locally-originated relay traffic such as discovery output.
3821    pub fn set_source_route_mode(
3822        &self,
3823        src: Option<RelaySideId>,
3824        mode: RouteSelectionMode,
3825    ) -> TelemetryResult<()> {
3826        let now_ms = self.clock.now_ms();
3827        let mut st = self.state.lock();
3828        if let Some(src) = src {
3829            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3830        }
3831        // Preserve explicit Fanout so discovery does not replace it with its
3832        // adaptive single-path default. Clearing is a separate operation.
3833        st.source_route_modes.insert(src, mode);
3834        st.route_selection_cursors.remove(&src);
3835        #[cfg(feature = "discovery")]
3836        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3837        Ok(())
3838    }
3839
3840    /// Clear a source-specific route-selection override.
3841    pub fn clear_source_route_mode(&self, src: Option<RelaySideId>) -> TelemetryResult<()> {
3842        let now_ms = self.clock.now_ms();
3843        let mut st = self.state.lock();
3844        if let Some(src) = src {
3845            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3846        }
3847        st.source_route_modes.remove(&src);
3848        st.route_selection_cursors.remove(&src);
3849        #[cfg(feature = "discovery")]
3850        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3851        Ok(())
3852    }
3853
3854    /// Set the weighted-routing weight from `src` toward `dst`.
3855    pub fn set_route_weight(
3856        &self,
3857        src: Option<RelaySideId>,
3858        dst: RelaySideId,
3859        weight: u32,
3860    ) -> TelemetryResult<()> {
3861        let now_ms = self.clock.now_ms();
3862        let mut st = self.state.lock();
3863        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3864        if let Some(src) = src {
3865            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3866        }
3867        st.route_weights.insert((src, dst), weight);
3868        st.route_selection_cursors.remove(&src);
3869        #[cfg(feature = "discovery")]
3870        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3871        Ok(())
3872    }
3873
3874    /// Clear a previously configured weighted-routing weight override.
3875    pub fn clear_route_weight(
3876        &self,
3877        src: Option<RelaySideId>,
3878        dst: RelaySideId,
3879    ) -> TelemetryResult<()> {
3880        let now_ms = self.clock.now_ms();
3881        let mut st = self.state.lock();
3882        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3883        if let Some(src) = src {
3884            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3885        }
3886        st.route_weights.remove(&(src, dst));
3887        st.route_selection_cursors.remove(&src);
3888        #[cfg(feature = "discovery")]
3889        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3890        Ok(())
3891    }
3892
3893    /// Set the failover priority from `src` toward `dst`.
3894    pub fn set_route_priority(
3895        &self,
3896        src: Option<RelaySideId>,
3897        dst: RelaySideId,
3898        priority: u32,
3899    ) -> TelemetryResult<()> {
3900        let now_ms = self.clock.now_ms();
3901        let mut st = self.state.lock();
3902        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3903        if let Some(src) = src {
3904            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3905        }
3906        st.route_priorities.insert((src, dst), priority);
3907        #[cfg(feature = "discovery")]
3908        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3909        Ok(())
3910    }
3911
3912    /// Clear a previously configured failover priority override.
3913    pub fn clear_route_priority(
3914        &self,
3915        src: Option<RelaySideId>,
3916        dst: RelaySideId,
3917    ) -> TelemetryResult<()> {
3918        let now_ms = self.clock.now_ms();
3919        let mut st = self.state.lock();
3920        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3921        if let Some(src) = src {
3922            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3923        }
3924        st.route_priorities.remove(&(src, dst));
3925        #[cfg(feature = "discovery")]
3926        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3927        Ok(())
3928    }
3929
3930    /// Allow or block routing from `src` toward `dst`.
3931    pub fn set_route(
3932        &self,
3933        src: Option<RelaySideId>,
3934        dst: RelaySideId,
3935        enabled: bool,
3936    ) -> TelemetryResult<()> {
3937        let now_ms = self.clock.now_ms();
3938        let mut st = self.state.lock();
3939        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3940        if let Some(src) = src {
3941            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3942        }
3943        st.route_overrides.insert((src, dst), enabled);
3944        #[cfg(feature = "discovery")]
3945        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3946        Ok(())
3947    }
3948
3949    /// Allow or block routing for a specific `DataType` from `src` toward `dst`.
3950    pub fn set_typed_route(
3951        &self,
3952        src: Option<RelaySideId>,
3953        ty: crate::DataType,
3954        dst: RelaySideId,
3955        enabled: bool,
3956    ) -> TelemetryResult<()> {
3957        let now_ms = self.clock.now_ms();
3958        let mut st = self.state.lock();
3959        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3960        if let Some(src) = src {
3961            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3962        }
3963        st.typed_route_overrides
3964            .insert((src, ty.as_u32(), dst), enabled);
3965        #[cfg(feature = "discovery")]
3966        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3967        Ok(())
3968    }
3969
3970    /// Clear a typed route override for the `(src, ty, dst)` triple.
3971    pub fn clear_typed_route(
3972        &self,
3973        src: Option<RelaySideId>,
3974        ty: crate::DataType,
3975        dst: RelaySideId,
3976    ) -> TelemetryResult<()> {
3977        let now_ms = self.clock.now_ms();
3978        let mut st = self.state.lock();
3979        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3980        if let Some(src) = src {
3981            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3982        }
3983        st.typed_route_overrides.remove(&(src, ty.as_u32(), dst));
3984        #[cfg(feature = "discovery")]
3985        Self::note_discovery_topology_change_locked(&mut st, now_ms);
3986        Ok(())
3987    }
3988
3989    /// Clear a non-typed route override so the relay falls back to default behavior.
3990    pub fn clear_route(&self, src: Option<RelaySideId>, dst: RelaySideId) -> TelemetryResult<()> {
3991        let now_ms = self.clock.now_ms();
3992        let mut st = self.state.lock();
3993        let _ = Self::side_ref(&st, dst).map_err(|_| TelemetryError::BadArg)?;
3994        if let Some(src) = src {
3995            let _ = Self::side_ref(&st, src).map_err(|_| TelemetryError::BadArg)?;
3996        }
3997        st.route_overrides.remove(&(src, dst));
3998        #[cfg(feature = "discovery")]
3999        Self::note_discovery_topology_change_locked(&mut st, now_ms);
4000        Ok(())
4001    }
4002
4003    #[cfg(feature = "discovery")]
4004    /// Queues an immediate discovery announcement for this relay.
4005    pub fn announce_discovery(&self) -> TelemetryResult<()> {
4006        self.queue_discovery_announce(true, false)
4007    }
4008
4009    /// Broadcast that this relay is leaving so peers can prune topology immediately.
4010    pub fn announce_leave(&self) -> TelemetryResult<()> {
4011        let pkt = discovery::build_discovery_leave("relay", self.clock.now_ms())?;
4012        let mut st = self.state.lock();
4013        let dsts: Vec<usize> = st
4014            .sides
4015            .iter()
4016            .enumerate()
4017            .filter_map(|(idx, side)| side.as_ref().map(|_| idx))
4018            .collect();
4019        for dst in dsts {
4020            let data = RelayItem::Packet(Arc::new(pkt.clone()));
4021            let priority = Self::relay_item_priority(&data)?;
4022            st.push_tx(RelayTxItem {
4023                src: None,
4024                dst,
4025                data,
4026                priority,
4027            })?;
4028        }
4029        Ok(())
4030    }
4031
4032    #[cfg(feature = "discovery")]
4033    /// Polls discovery state and queues an announce if the cadence says one is due.
4034    pub fn poll_discovery(&self) -> TelemetryResult<bool> {
4035        self.poll_discovery_announce()
4036    }
4037
4038    #[cfg(feature = "discovery")]
4039    /// Exports the relay's current discovered topology snapshot.
4040    pub fn export_topology(&self) -> TopologySnapshot {
4041        let now_ms = self.clock.now_ms();
4042        let mut st = self.state.lock();
4043        if Self::prune_discovery_routes_locked(&mut st, now_ms) {
4044            self.reconcile_end_to_end_acked_destinations_locked(&mut st);
4045            Self::note_discovery_topology_change_locked(&mut st, now_ms);
4046        }
4047        let routes = st
4048            .discovery_routes
4049            .iter()
4050            .filter_map(|(&side_id, route)| {
4051                let side = st.sides.get(side_id).and_then(|side| side.as_ref())?;
4052                let announcers = route
4053                    .announcers
4054                    .iter()
4055                    .map(|(sender_id, sender_state)| TopologyAnnouncerRoute {
4056                        sender_id: sender_id.clone(),
4057                        reachable_endpoints: sender_state
4058                            .reachable
4059                            .iter()
4060                            .copied()
4061                            .filter(|ep| !discovery::is_router_control_endpoint(*ep))
4062                            .collect(),
4063                        reachable_network_variables: Vec::new(),
4064                        reachable_timesync_sources: sender_state.reachable_timesync_sources.clone(),
4065                        routers: sender_state.topology_boards.clone(),
4066                        last_seen_ms: sender_state.last_seen_ms,
4067                        age_ms: now_ms.saturating_sub(sender_state.last_seen_ms),
4068                    })
4069                    .collect();
4070                Some(TopologySideRoute {
4071                    side_id,
4072                    side_name: side.name.to_string(),
4073                    reachable_endpoints: route
4074                        .reachable
4075                        .iter()
4076                        .copied()
4077                        .filter(|ep| !discovery::is_router_control_endpoint(*ep))
4078                        .collect(),
4079                    reachable_network_variables: Vec::new(),
4080                    reachable_timesync_sources: route.reachable_timesync_sources.clone(),
4081                    announcers,
4082                    last_seen_ms: route.last_seen_ms,
4083                    age_ms: now_ms.saturating_sub(route.last_seen_ms),
4084                })
4085            })
4086            .collect();
4087        let routers = self.advertised_discovery_topology_for_link_locked(&st, now_ms, true);
4088        let advertised_endpoints =
4089            self.advertised_discovery_endpoints_for_link_locked(&st, now_ms, true);
4090        let advertised_timesync_sources =
4091            self.advertised_discovery_timesync_sources_for_link_locked(&st, now_ms);
4092        let links = discovery::topology_links_from_boards(&routers);
4093        TopologySnapshot {
4094            advertised_endpoints,
4095            advertised_timesync_sources,
4096            routers,
4097            links,
4098            routes,
4099            current_announce_interval_ms: st.discovery_cadence.current_interval_ms,
4100            next_announce_ms: st.discovery_cadence.next_announce_ms,
4101        }
4102    }
4103
4104    #[cfg(feature = "discovery")]
4105    pub fn client_stats(&self, sender_id: &str) -> Option<ClientStatsSnapshot> {
4106        let now_ms = self.clock.now_ms();
4107        let st = self.state.lock();
4108        let mut side_ids = Vec::new();
4109        let mut side_names = Vec::new();
4110        let mut last_seen_ms = None::<u64>;
4111        let mut reachable_endpoints = Vec::new();
4112        let mut reachable_timesync_sources = Vec::new();
4113        let mut packets_sent = 0u64;
4114        let mut packets_received = 0u64;
4115        let mut bytes_sent = 0u64;
4116        let mut bytes_received = 0u64;
4117
4118        for (side_id, route) in &st.discovery_routes {
4119            let Some(sender_state) = route.announcers.get(sender_id) else {
4120                continue;
4121            };
4122            side_ids.push(*side_id);
4123            if let Some(side_name) = st
4124                .sides
4125                .get(*side_id)
4126                .and_then(|side| side.as_ref())
4127                .map(|side| side.name.clone())
4128            {
4129                side_names.push(side_name.to_string());
4130            }
4131            last_seen_ms = Some(last_seen_ms.unwrap_or(0).max(sender_state.last_seen_ms));
4132            reachable_endpoints.extend(sender_state.reachable.iter().copied());
4133            reachable_timesync_sources
4134                .extend(sender_state.reachable_timesync_sources.iter().cloned());
4135            if let Some(stats) = st.side_runtime_stats.get(side_id) {
4136                packets_sent = packets_sent.saturating_add(stats.tx_packets);
4137                packets_received = packets_received.saturating_add(stats.rx_packets);
4138                bytes_sent = bytes_sent.saturating_add(stats.tx_bytes);
4139                bytes_received = bytes_received.saturating_add(stats.rx_bytes);
4140            }
4141        }
4142
4143        if side_ids.is_empty() {
4144            return None;
4145        }
4146        reachable_endpoints.retain(|ep| !discovery::is_router_control_endpoint(*ep));
4147        reachable_endpoints.sort_unstable();
4148        reachable_endpoints.dedup();
4149        reachable_timesync_sources.sort_unstable();
4150        reachable_timesync_sources.dedup();
4151        side_ids.sort_unstable();
4152        side_ids.dedup();
4153        side_names.sort_unstable();
4154        side_names.dedup();
4155        let age_ms = last_seen_ms.map(|seen| now_ms.saturating_sub(seen));
4156        Some(ClientStatsSnapshot {
4157            sender_id: sender_id.to_string(),
4158            connected: age_ms.is_some_and(|age| age <= DISCOVERY_ROUTE_TTL_MS),
4159            side_ids,
4160            side_names,
4161            last_seen_ms,
4162            age_ms,
4163            reachable_endpoints,
4164            reachable_timesync_sources,
4165            packets_sent,
4166            packets_received,
4167            bytes_sent,
4168            bytes_received,
4169        })
4170    }
4171
4172    pub fn export_runtime_stats(&self) -> RuntimeStatsSnapshot {
4173        let now_ms = self.clock.now_ms();
4174        let st = self.state.lock();
4175
4176        let mut sides = Vec::new();
4177        for (side_id, side) in st.sides.iter().enumerate() {
4178            let Some(side) = side.as_ref() else { continue };
4179            let stats = st
4180                .side_runtime_stats
4181                .get(&side_id)
4182                .cloned()
4183                .unwrap_or_default();
4184            let adaptive = st
4185                .adaptive_route_stats
4186                .get(&side_id)
4187                .cloned()
4188                .unwrap_or_default()
4189                .snapshot(now_ms, true);
4190            let (tx_template_count, rx_template_count) = st
4191                .side_transport
4192                .get(&side_id)
4193                .map(|state| (state.tx_template_count(), state.rx_template_count()))
4194                .unwrap_or((0, 0));
4195            let mut data_types: Vec<RuntimeTypeStats> = stats
4196                .data_types
4197                .into_iter()
4198                .map(|(ty, item)| RuntimeTypeStats {
4199                    data_type: crate::DataType(ty),
4200                    tx_packets: item.tx_packets,
4201                    tx_bytes: item.tx_bytes,
4202                    rx_packets: item.rx_packets,
4203                    rx_bytes: item.rx_bytes,
4204                    relayed_tx_packets: item.relayed_tx_packets,
4205                    relayed_tx_bytes: item.relayed_tx_bytes,
4206                    relayed_rx_packets: item.relayed_rx_packets,
4207                    relayed_rx_bytes: item.relayed_rx_bytes,
4208                    tx_retries: item.tx_retries,
4209                    handler_failures: item.handler_failures,
4210                })
4211                .collect();
4212            data_types.sort_unstable_by_key(|item| item.data_type.as_u32());
4213            sides.push(RuntimeSideStats {
4214                side_id,
4215                side_name: side.name.to_string(),
4216                reliable_enabled: side.opts.reliable_enabled,
4217                link_local_enabled: side.opts.link_local_enabled,
4218                header_template_enabled: side.opts.header_template_enabled,
4219                max_frame_bytes: side.opts.max_frame_bytes,
4220                compact_header_target_bytes: side.opts.compact_header_target_bytes,
4221                side_transport_profile: side.opts.effective_transport_profile().as_str(),
4222                ingress_enabled: side.opts.ingress_enabled,
4223                egress_enabled: side.opts.egress_enabled,
4224                tx_packets: stats.tx_packets,
4225                tx_bytes: stats.tx_bytes,
4226                rx_packets: stats.rx_packets,
4227                rx_bytes: stats.rx_bytes,
4228                relayed_tx_packets: stats.relayed_tx_packets,
4229                relayed_tx_bytes: stats.relayed_tx_bytes,
4230                relayed_rx_packets: stats.relayed_rx_packets,
4231                relayed_rx_bytes: stats.relayed_rx_bytes,
4232                local_delivery_packets: 0,
4233                tx_retries: stats.tx_retries,
4234                tx_handler_failures: stats.tx_handler_failures,
4235                local_handler_failures: 0,
4236                total_handler_retries: stats.total_handler_retries,
4237                side_transport_full_frames: stats.side_transport_full_frames,
4238                side_transport_compact_frames: stats.side_transport_compact_frames,
4239                side_transport_compact_delta_frames: stats.side_transport_compact_delta_frames,
4240                side_transport_compact_omitted_timestamp_frames: stats
4241                    .side_transport_compact_omitted_timestamp_frames,
4242                side_transport_chunk_frames: stats.side_transport_chunk_frames,
4243                side_transport_raw_bytes: stats.side_transport_raw_bytes,
4244                side_transport_wire_bytes: stats.side_transport_wire_bytes,
4245                side_transport_bytes_saved: stats.side_transport_bytes_saved,
4246                side_transport_min_compact_overhead_bytes: stats
4247                    .side_transport_min_compact_overhead_bytes,
4248                side_transport_max_compact_overhead_bytes: stats
4249                    .side_transport_max_compact_overhead_bytes,
4250                side_transport_compact_target_misses: stats.side_transport_compact_target_misses,
4251                side_transport_template_evictions: stats.side_transport_template_evictions,
4252                side_transport_tx_template_count: tx_template_count,
4253                side_transport_rx_template_count: rx_template_count,
4254                max_side_transport_templates: side.opts.max_side_transport_templates,
4255                adaptive,
4256                data_types,
4257            });
4258        }
4259
4260        let mut route_modes: Vec<RouteModeStats> = st
4261            .route_selection_cursors
4262            .iter()
4263            .map(|(src, cursor)| RouteModeStats {
4264                src_side_id: *src,
4265                selection_mode: st.source_route_modes.get(src).copied(),
4266                cursor: *cursor,
4267            })
4268            .collect();
4269        for src in st.source_route_modes.keys() {
4270            if !route_modes.iter().any(|mode| mode.src_side_id == *src) {
4271                route_modes.push(RouteModeStats {
4272                    src_side_id: *src,
4273                    selection_mode: st.source_route_modes.get(src).copied(),
4274                    cursor: 0,
4275                });
4276            }
4277        }
4278        route_modes.sort_unstable_by_key(|mode| mode.src_side_id.unwrap_or(usize::MAX));
4279
4280        let mut route_overrides: Vec<RouteOverrideStats> = st
4281            .route_overrides
4282            .iter()
4283            .map(|((src, dst), enabled)| RouteOverrideStats {
4284                src_side_id: *src,
4285                dst_side_id: *dst,
4286                enabled: *enabled,
4287            })
4288            .collect();
4289        route_overrides.sort_unstable_by_key(|item| {
4290            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
4291        });
4292
4293        let mut typed_route_overrides: Vec<TypedRouteOverrideStats> = st
4294            .typed_route_overrides
4295            .iter()
4296            .map(|((src, ty, dst), enabled)| TypedRouteOverrideStats {
4297                src_side_id: *src,
4298                data_type: crate::DataType(*ty),
4299                dst_side_id: *dst,
4300                enabled: *enabled,
4301            })
4302            .collect();
4303        typed_route_overrides.sort_unstable_by_key(|item| {
4304            (
4305                item.src_side_id.unwrap_or(usize::MAX),
4306                item.data_type.as_u32(),
4307                item.dst_side_id,
4308            )
4309        });
4310
4311        let mut route_weights: Vec<RouteWeightStats> = st
4312            .route_weights
4313            .iter()
4314            .map(|((src, dst), weight)| RouteWeightStats {
4315                src_side_id: *src,
4316                dst_side_id: *dst,
4317                weight: *weight,
4318            })
4319            .collect();
4320        route_weights.sort_unstable_by_key(|item| {
4321            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
4322        });
4323
4324        let mut route_priorities: Vec<RoutePriorityStats> = st
4325            .route_priorities
4326            .iter()
4327            .map(|((src, dst), priority)| RoutePriorityStats {
4328                src_side_id: *src,
4329                dst_side_id: *dst,
4330                priority: *priority,
4331            })
4332            .collect();
4333        route_priorities.sort_unstable_by_key(|item| {
4334            (item.src_side_id.unwrap_or(usize::MAX), item.dst_side_id)
4335        });
4336
4337        #[cfg(feature = "discovery")]
4338        let discovery = DiscoveryRuntimeStats {
4339            route_count: st.discovery_routes.len(),
4340            announcer_count: st
4341                .discovery_routes
4342                .values()
4343                .map(|route| route.announcers.len())
4344                .sum(),
4345            current_announce_interval_ms: Some(st.discovery_cadence.current_interval_ms),
4346            next_announce_ms: Some(st.discovery_cadence.next_announce_ms),
4347        };
4348        #[cfg(not(feature = "discovery"))]
4349        let discovery = DiscoveryRuntimeStats {
4350            route_count: 0,
4351            announcer_count: 0,
4352            current_announce_interval_ms: None,
4353            next_announce_ms: None,
4354        };
4355
4356        RuntimeStatsSnapshot {
4357            sides,
4358            route_modes,
4359            route_overrides,
4360            typed_route_overrides,
4361            route_weights,
4362            route_priorities,
4363            queues: QueueRuntimeStats {
4364                rx_len: st.rx_queue.len(),
4365                rx_bytes: st.rx_queue.bytes_used(),
4366                tx_len: st.tx_queue.len(),
4367                tx_bytes: st.tx_queue.bytes_used(),
4368                replay_len: st.replay_queue.len(),
4369                replay_bytes: st.replay_queue.bytes_used(),
4370                recent_rx_len: st.recent_rx.len(),
4371                recent_rx_bytes: st.recent_rx.bytes_used(),
4372                reliable_rx_buffered_len: st.reliable_rx_buffer_len(),
4373                reliable_rx_buffered_bytes: st.reliable_rx_buffered_bytes(),
4374                shared_queue_bytes_used: st.shared_queue_bytes_used(),
4375            },
4376            reliable: ReliableRuntimeStats {
4377                reliable_return_route_count: st.reliable_return_routes.len(),
4378                end_to_end_pending_count: 0,
4379                end_to_end_pending_destination_count: 0,
4380                end_to_end_acked_cache_count: st.end_to_end_acked_destinations.len(),
4381            },
4382            discovery,
4383            total_handler_failures: st.total_handler_failures,
4384            total_handler_retries: st.total_handler_retries,
4385        }
4386    }
4387
4388    /// Export current relay memory usage/layout as JSON for profiling.
4389    pub fn export_memory_layout_json(&self) -> String {
4390        let st = self.state.lock();
4391        #[cfg(feature = "discovery")]
4392        let discovery_bytes = st.discovery_bytes_used();
4393        #[cfg(not(feature = "discovery"))]
4394        let discovery_bytes = 0usize;
4395        let schema_bytes = crate::config::schema_bytes_used();
4396        let memory = st.memory;
4397        let mut out = String::new();
4398        let _ = core::fmt::Write::write_fmt(
4399            &mut out,
4400            format_args!(
4401                "{{\"kind\":\"relay\",\
4402                 \"shared_queue_bytes_used\":{},\"shared_queue_bytes_allocated\":{},\
4403                 \"rx_queue_bytes_used\":{},\"rx_queue_bytes_allocated\":{},\"rx_queue_len\":{},\
4404                 \"tx_queue_bytes_used\":{},\"tx_queue_bytes_allocated\":{},\"tx_queue_len\":{},\
4405                 \"replay_queue_bytes_used\":{},\"replay_queue_bytes_allocated\":{},\"replay_queue_len\":{},\
4406                 \"recent_rx_bytes_used\":{},\"recent_rx_bytes_allocated\":{},\"recent_rx_len\":{},\
4407                 \"reliable_rx_buffer_bytes_used\":{},\"reliable_rx_buffer_bytes_allocated\":{},\"reliable_rx_buffer_len\":{},\
4408                 \"discovery_bytes_used\":{},\"discovery_bytes_allocated\":{},\
4409                 \"schema_bytes_used\":{},\"schema_bytes_allocated\":{}}}",
4410                st.shared_queue_bytes_used(),
4411                memory.max_queue_budget,
4412                st.rx_queue.bytes_used(),
4413                st.rx_queue.max_bytes(),
4414                st.rx_queue.len(),
4415                st.tx_queue.bytes_used(),
4416                st.tx_queue.max_bytes(),
4417                st.tx_queue.len(),
4418                st.replay_queue.bytes_used(),
4419                st.replay_queue.max_bytes(),
4420                st.replay_queue.len(),
4421                st.recent_rx.bytes_used(),
4422                st.recent_rx.max_bytes(),
4423                st.recent_rx.len(),
4424                st.reliable_rx_buffered_bytes(),
4425                memory.max_queue_budget,
4426                st.reliable_rx_buffer_len(),
4427                discovery_bytes,
4428                memory.max_queue_budget,
4429                schema_bytes,
4430                memory.max_queue_budget,
4431            ),
4432        );
4433        out
4434    }
4435
4436    #[cfg(test)]
4437    pub(crate) fn debug_end_to_end_acked_destination_count(&self, packet_id: u64) -> Option<usize> {
4438        let st = self.state.lock();
4439        st.end_to_end_acked_destinations
4440            .get(&packet_id)
4441            .map(BTreeSet::len)
4442    }
4443
4444    #[cfg(test)]
4445    pub(crate) fn debug_end_to_end_acked_packet_count(&self) -> usize {
4446        let st = self.state.lock();
4447        st.end_to_end_acked_destinations.len()
4448    }
4449
4450    #[cfg(test)]
4451    pub(crate) fn debug_reliable_return_route_count(&self) -> usize {
4452        let st = self.state.lock();
4453        st.reliable_return_routes.len()
4454    }
4455
4456    #[cfg(test)]
4457    pub(crate) fn debug_side_storage(&self) -> (usize, usize) {
4458        let st = self.state.lock();
4459        (st.sides.len(), st.sides.capacity())
4460    }
4461
4462    /// Enqueue packed bytes that originated from `src` into the relay RX queue.
4463    ///
4464    /// Note: `Arc::from(bytes)` allocates and copies `len` bytes into a new `Arc<[u8]>`.
4465    /// This is still “fast enough” for many cases, but it is not allocation-free / ISR-safe.
4466    pub fn rx_packed_from_side(&self, src: RelaySideId, bytes: &[u8]) -> TelemetryResult<()> {
4467        self.ensure_side_ingress_enabled(src)?;
4468        let Some(bytes) = self.decode_side_transport_frame(src, bytes)? else {
4469            return Ok(());
4470        };
4471        let mut st = self.state.lock();
4472
4473        let data = RelayItem::Packed(bytes);
4474        let priority = Self::relay_item_priority(&data)?;
4475        st.push_rx(RelayRxItem {
4476            src,
4477            data,
4478            priority,
4479        })
4480    }
4481
4482    /// Enqueue a full packet that originated from `src` into the relay RX queue.
4483    ///
4484    /// The packet is wrapped in `Arc<Packet>` so fanout can clone the pointer cheaply.
4485    pub fn rx_from_side(&self, src: RelaySideId, packet: Packet) -> TelemetryResult<()> {
4486        self.ensure_side_ingress_enabled(src)?;
4487        let mut st = self.state.lock();
4488
4489        let data = RelayItem::Packet(Arc::new(packet));
4490        let priority = Self::relay_item_priority(&data)?;
4491        st.push_rx(RelayRxItem {
4492            src,
4493            data,
4494            priority,
4495        })
4496    }
4497
4498    /// Clear both RX and TX queues.
4499    pub fn clear_queues(&self) {
4500        let mut st = self.state.lock();
4501        st.rx_queue.clear();
4502        st.tx_queue.clear();
4503        st.tx_priority_burst = 0;
4504    }
4505
4506    /// Clear only RX queue.
4507    pub fn clear_rx_queue(&self) {
4508        let mut st = self.state.lock();
4509        st.rx_queue.clear();
4510    }
4511
4512    /// Clear only TX queue.
4513    pub fn clear_tx_queue(&self) {
4514        let mut st = self.state.lock();
4515        st.tx_queue.clear();
4516        st.tx_priority_burst = 0;
4517        st.replay_queue.clear();
4518    }
4519
4520    /// Internal: expand one RX item into TX items for all other sides.
4521    ///
4522    /// Fanout is cheap: the `RelayItem` is cloned (Arc bump) and reused across all destinations.
4523    fn process_rx_queue_item(&self, item: RelayRxItem) -> TelemetryResult<()> {
4524        self.ensure_side_ingress_enabled(item.src)?;
4525        match &item.data {
4526            RelayItem::Packet(pkt) => {
4527                let bytes = wire_format::pack_packet(pkt).len();
4528                self.note_side_rx(item.src, pkt.data_type(), bytes);
4529            }
4530            RelayItem::Packed(bytes) => {
4531                if let Ok(env) = wire_format::peek_envelope(bytes.as_ref()) {
4532                    self.note_side_rx(item.src, env.ty, bytes.len());
4533                }
4534            }
4535        }
4536        match &item.data {
4537            RelayItem::Packet(pkt) => {
4538                if is_reliable_type(pkt.data_type()) && !is_internal_control_type(pkt.data_type()) {
4539                    self.note_reliable_return_route(item.src, pkt.packet_id());
4540                }
4541            }
4542            RelayItem::Packed(bytes) => {
4543                if let Ok(env) = wire_format::peek_envelope(bytes.as_ref())
4544                    && is_reliable_type(env.ty)
4545                    && !is_internal_control_type(env.ty)
4546                    && let Ok(packet_id) = wire_format::packet_id_from_wire(bytes.as_ref())
4547                {
4548                    self.note_reliable_return_route(item.src, packet_id);
4549                }
4550            }
4551        }
4552        let mut released_buffered: Vec<Arc<[u8]>> = Vec::new();
4553        if let RelayItem::Packed(bytes) = &item.data {
4554            let (_opts, handler_is_packed, hop_reliable_enabled) = {
4555                let st = self.state.lock();
4556                let side_ref = Self::side_ref(&st, item.src)?;
4557                let opts = side_ref.opts;
4558                (
4559                    opts,
4560                    matches!(side_ref.tx_handler, RelayTxHandlerFn::Packed(_)),
4561                    opts.reliable_enabled
4562                        && !self.side_has_multiple_announcers_locked(
4563                            &st,
4564                            item.src,
4565                            self.clock.now_ms(),
4566                        ),
4567                )
4568            };
4569
4570            let frame = match wire_format::peek_frame_info(bytes.as_ref()) {
4571                Ok(frame) => frame,
4572                Err(e) => {
4573                    if matches!(e, TelemetryError::Unpack(msg) if msg == "crc32 mismatch")
4574                        && hop_reliable_enabled
4575                        && handler_is_packed
4576                        && let Ok(frame) = wire_format::peek_frame_info_unchecked(bytes.as_ref())
4577                    {
4578                        if is_reliable_type(frame.envelope.ty)
4579                            && let Some(hdr) = frame.reliable
4580                        {
4581                            let unordered = (hdr.flags & wire_format::RELIABLE_FLAG_UNORDERED) != 0;
4582                            let unsequenced =
4583                                (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) != 0;
4584
4585                            if !unsequenced {
4586                                let requested = if unordered {
4587                                    hdr.seq
4588                                } else {
4589                                    let mut st = self.state.lock();
4590                                    let rx_state = self.reliable_rx_state_mut(
4591                                        &mut st,
4592                                        item.src,
4593                                        frame.envelope.ty,
4594                                    );
4595                                    rx_state.expected_seq.min(hdr.seq)
4596                                };
4597                                self.queue_reliable_packet_request(
4598                                    item.src,
4599                                    frame.envelope.ty,
4600                                    requested,
4601                                )?;
4602                            }
4603                        }
4604                        return Ok(());
4605                    }
4606                    return Err(e);
4607                }
4608            };
4609
4610            if hop_reliable_enabled
4611                && handler_is_packed
4612                && is_reliable_type(frame.envelope.ty)
4613                && let Some(hdr) = frame.reliable
4614            {
4615                if frame.ack_only() {
4616                    self.handle_reliable_ack(item.src, frame.envelope.ty, hdr.ack);
4617                    return Ok(());
4618                }
4619                let unordered = (hdr.flags & wire_format::RELIABLE_FLAG_UNORDERED) != 0;
4620                let unsequenced = (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) != 0;
4621
4622                if !unsequenced {
4623                    if unordered {
4624                        self.queue_reliable_ack(item.src, frame.envelope.ty, hdr.seq)?;
4625                    } else {
4626                        let mut release: Vec<Arc<[u8]>> = Vec::new();
4627                        let mut last_delivered = None;
4628                        let mut ack_old = None;
4629                        let mut request_missing = None;
4630                        let mut partial_ack = None;
4631                        {
4632                            let mut st = self.state.lock();
4633                            let rx_state =
4634                                self.reliable_rx_state_mut(&mut st, item.src, frame.envelope.ty);
4635                            let expected_seq = rx_state.expected_seq;
4636                            if hdr.seq < expected_seq {
4637                                ack_old = Some(expected_seq.saturating_sub(1));
4638                            } else if hdr.seq > expected_seq {
4639                                request_missing = Some(expected_seq);
4640                                partial_ack = Some(hdr.seq);
4641                                st.buffer_reliable_rx(
4642                                    item.src,
4643                                    frame.envelope.ty,
4644                                    hdr.seq,
4645                                    bytes.clone(),
4646                                )?;
4647                            } else {
4648                                release.push(bytes.clone());
4649                                last_delivered = Some(hdr.seq);
4650                                let mut next_expected = hdr.seq.wrapping_add(1);
4651                                while let Some(buf) = rx_state.buffered.remove(&next_expected) {
4652                                    release.push(buf);
4653                                    last_delivered = Some(next_expected);
4654                                    let next = next_expected.wrapping_add(1);
4655                                    next_expected = if next == 0 { 1 } else { next };
4656                                }
4657                                rx_state.expected_seq = next_expected;
4658                            }
4659                        }
4660
4661                        if let Some(ack_seq) = ack_old {
4662                            self.queue_reliable_ack(item.src, frame.envelope.ty, ack_seq)?;
4663                            return Ok(());
4664                        }
4665                        if let Some(request_seq) = request_missing {
4666                            if let Some(partial_seq) = partial_ack {
4667                                self.queue_reliable_partial_ack(
4668                                    item.src,
4669                                    frame.envelope.ty,
4670                                    partial_seq,
4671                                )?;
4672                            }
4673                            self.queue_reliable_packet_request(
4674                                item.src,
4675                                frame.envelope.ty,
4676                                request_seq,
4677                            )?;
4678                            return Ok(());
4679                        }
4680                        if let Some(ack_seq) = last_delivered {
4681                            self.queue_reliable_ack(item.src, frame.envelope.ty, ack_seq)?;
4682                        }
4683                        released_buffered.extend(release.into_iter().skip(1));
4684                    }
4685                }
4686            }
4687        }
4688
4689        if self.is_duplicate_pkt(&item)? && !self.should_forward_duplicate_reliable_item(&item)? {
4690            // Already fanned out this packet recently; skip.
4691            return Ok(());
4692        }
4693
4694        self.dispatch_relay_rx_item(&item)?;
4695
4696        for release_bytes in released_buffered {
4697            let release_item = RelayRxItem {
4698                src: item.src,
4699                priority: Self::relay_item_priority(&RelayItem::Packed(release_bytes.clone()))?,
4700                data: RelayItem::Packed(release_bytes),
4701            };
4702            if self.is_duplicate_pkt(&release_item)?
4703                && !self.should_forward_duplicate_reliable_item(&release_item)?
4704            {
4705                continue;
4706            }
4707            self.dispatch_relay_rx_item(&release_item)?;
4708        }
4709        Ok(())
4710    }
4711
4712    fn dispatch_relay_rx_item(&self, item: &RelayRxItem) -> TelemetryResult<()> {
4713        match &item.data {
4714            RelayItem::Packet(pkt) => {
4715                if matches!(
4716                    pkt.data_type(),
4717                    crate::DataType::ReliableAck
4718                        | crate::DataType::ReliablePartialAck
4719                        | crate::DataType::ReliablePacketRequest
4720                ) {
4721                    if pkt.data_type() == crate::DataType::ReliableAck
4722                        && Self::is_end_to_end_ack_sender(pkt.sender())
4723                        && Self::decode_end_to_end_reliable_ack(pkt.payload()).is_ok()
4724                    {
4725                        if let Ok(packet_id) = Self::decode_end_to_end_reliable_ack(pkt.payload())
4726                            && let Some(sender_hash) =
4727                                Self::decode_end_to_end_ack_sender_hash(pkt.sender())
4728                        {
4729                            let mut st = self.state.lock();
4730                            Self::note_end_to_end_acked_destination_locked(
4731                                &mut st,
4732                                packet_id,
4733                                sender_hash,
4734                            );
4735                        }
4736                    } else {
4737                        let vals = pkt.data_as_u32()?;
4738                        if vals.len() != 2 {
4739                            return Err(TelemetryError::Unpack("bad reliable control payload"));
4740                        }
4741                        let ty = crate::DataType::try_from_u32(vals[0])
4742                            .ok_or(TelemetryError::InvalidType)?;
4743                        let seq = vals[1];
4744                        match pkt.data_type() {
4745                            crate::DataType::ReliableAck => {
4746                                self.handle_reliable_ack(item.src, ty, seq)
4747                            }
4748                            crate::DataType::ReliablePartialAck => {
4749                                self.handle_reliable_partial_ack(item.src, ty, seq)
4750                            }
4751                            crate::DataType::ReliablePacketRequest => {
4752                                self.queue_reliable_retransmit(item.src, ty, seq)?
4753                            }
4754                            _ => {}
4755                        }
4756                        return Ok(());
4757                    }
4758                }
4759            }
4760            RelayItem::Packed(bytes) => {
4761                let env = wire_format::peek_envelope(bytes.as_ref())?;
4762                if matches!(
4763                    env.ty,
4764                    crate::DataType::ReliableAck
4765                        | crate::DataType::ReliablePacketRequest
4766                        | crate::DataType::ReliablePartialAck
4767                ) {
4768                    let pkt = wire_format::unpack_packet(bytes.as_ref())?;
4769                    return self.dispatch_relay_rx_item(&RelayRxItem {
4770                        src: item.src,
4771                        data: RelayItem::Packet(Arc::new(pkt)),
4772                        priority: item.priority,
4773                    });
4774                }
4775            }
4776        }
4777
4778        let src = item.src;
4779        let data = item.data.clone();
4780        self.learn_discovery_item(src, &data)?;
4781
4782        let plan = self.remote_side_plan(&data, src)?;
4783        let mut st = self.state.lock();
4784        let RemoteSidePlan::Target(sides) = plan;
4785        for dst in sides {
4786            let priority = Self::relay_item_priority(&data)?;
4787            st.push_tx(RelayTxItem {
4788                src: Some(src),
4789                dst,
4790                data: data.clone(),
4791                priority,
4792            })?;
4793        }
4794        Ok(())
4795    }
4796
4797    #[inline]
4798    fn crc32_bytes(data: &[u8]) -> u32 {
4799        let mut hasher = Crc32Hasher::new();
4800        hasher.update(data);
4801        hasher.finalize()
4802    }
4803
4804    fn wrap_side_transport_frame(kind: u8, body: &[u8]) -> Arc<[u8]> {
4805        let mut out = Vec::with_capacity(
4806            SIDE_TRANSPORT_MAGIC.len() + 1 + body.len() + wire_format::CRC32_BYTES,
4807        );
4808        out.extend_from_slice(SIDE_TRANSPORT_MAGIC);
4809        out.push(kind);
4810        out.extend_from_slice(body);
4811        let crc = Self::crc32_bytes(&out);
4812        out.extend_from_slice(&crc.to_le_bytes());
4813        Arc::from(out)
4814    }
4815
4816    fn parse_side_transport_wrapper(bytes: &[u8]) -> TelemetryResult<Option<(u8, &[u8])>> {
4817        if bytes.len() < SIDE_TRANSPORT_MAGIC.len() + 1 + wire_format::CRC32_BYTES {
4818            return Ok(None);
4819        }
4820        if &bytes[..SIDE_TRANSPORT_MAGIC.len()] != SIDE_TRANSPORT_MAGIC {
4821            return Ok(None);
4822        }
4823        let data_len = bytes.len() - wire_format::CRC32_BYTES;
4824        let expected = u32::from_le_bytes([
4825            bytes[data_len],
4826            bytes[data_len + 1],
4827            bytes[data_len + 2],
4828            bytes[data_len + 3],
4829        ]);
4830        let data = &bytes[..data_len];
4831        if Self::crc32_bytes(data) != expected {
4832            return Err(TelemetryError::Unpack("side transport crc32 mismatch"));
4833        }
4834        let kind = data[SIDE_TRANSPORT_MAGIC.len()];
4835        Ok(Some((kind, &data[SIDE_TRANSPORT_MAGIC.len() + 1..])))
4836    }
4837
4838    fn read_uleb128_local(buf: &[u8], off: &mut usize) -> TelemetryResult<u64> {
4839        let mut result = 0u64;
4840        let mut shift = 0u32;
4841        for _ in 0..10 {
4842            let byte = *buf.get(*off).ok_or(TelemetryError::Unpack("short read"))?;
4843            *off += 1;
4844            result |= u64::from(byte & 0x7F) << shift;
4845            if (byte & 0x80) == 0 {
4846                return Ok(result);
4847            }
4848            shift += 7;
4849        }
4850        Err(TelemetryError::Unpack("uleb128 too long"))
4851    }
4852
4853    fn write_uleb128_local(mut value: u64, out: &mut Vec<u8>) {
4854        loop {
4855            let mut byte = (value & 0x7F) as u8;
4856            value >>= 7;
4857            if value != 0 {
4858                byte |= 0x80;
4859            }
4860            out.push(byte);
4861            if value == 0 {
4862                break;
4863            }
4864        }
4865    }
4866
4867    fn uleb128_len_local(mut value: u64) -> usize {
4868        let mut len = 1;
4869        while value >= 0x80 {
4870            value >>= 7;
4871            len += 1;
4872        }
4873        len
4874    }
4875
4876    fn extract_side_header_template(bytes: &[u8]) -> TelemetryResult<SideTemplateExtract<'_>> {
4877        if bytes.len() < wire_format::CRC32_BYTES + 4 {
4878            return Err(TelemetryError::Unpack("short buffer"));
4879        }
4880        let data_len = bytes.len() - wire_format::CRC32_BYTES;
4881        let data = &bytes[..data_len];
4882        let mut off = 0usize;
4883        let flags = *data
4884            .get(off)
4885            .ok_or(TelemetryError::Unpack("short prelude"))?;
4886        off += 1;
4887        off += 1; // NEP
4888        let ty_u64 = Self::read_uleb128_local(data, &mut off)?;
4889        let ty_u32 = u32::try_from(ty_u64).map_err(|_| TelemetryError::Unpack("bad data type"))?;
4890        if ty_u32 > crate::MAX_VALUE_DATA_TYPE {
4891            return Err(TelemetryError::Unpack("bad data type"));
4892        }
4893        let ty = crate::DataType(ty_u32);
4894        let data_size_off = off;
4895        let data_size = Self::read_uleb128_local(data, &mut off)?;
4896        let timestamp = Self::read_uleb128_local(data, &mut off)?;
4897        let nonce = if (flags & SIDE_TRANSPORT_FLAG_PACKET_NONCE) != 0 {
4898            u16::try_from(Self::read_uleb128_local(data, &mut off)?)
4899                .map_err(|_| TelemetryError::Unpack("packet nonce too large"))?
4900        } else {
4901            0
4902        };
4903        let between_start = off;
4904        let _source_address = u32::try_from(Self::read_uleb128_local(data, &mut off)?)
4905            .map_err(|_| TelemetryError::Unpack("source address too large"))?;
4906        let endpoint_bitmap_bytes = if (flags & SIDE_TRANSPORT_FLAG_ENDPOINT_BITMAP_PRESENT) != 0 {
4907            SIDE_TRANSPORT_EP_BITMAP_BYTES
4908        } else {
4909            0
4910        };
4911        if data.len() < off + endpoint_bitmap_bytes {
4912            return Err(TelemetryError::Unpack("short buffer"));
4913        }
4914        off += endpoint_bitmap_bytes;
4915        if (flags & SIDE_TRANSPORT_FLAG_WIRE_CONTRACT) != 0 {
4916            let contract_len = usize::try_from(Self::read_uleb128_local(data, &mut off)?)
4917                .map_err(|_| TelemetryError::Unpack("wire contract length"))?;
4918            if data.len() < off + contract_len {
4919                return Err(TelemetryError::Unpack("short buffer"));
4920            }
4921            off += contract_len;
4922        }
4923        let reliable_span = wire_format::reliable_header_span(bytes)?;
4924        let (reliable_flags, reliable_seq_ack, reliable_compact, payload_off) =
4925            if let Some((rel_off, rel_len, hdr)) = reliable_span {
4926                if data.len() < rel_off + rel_len {
4927                    return Err(TelemetryError::Unpack("short buffer"));
4928                }
4929                (
4930                    Some(hdr.flags),
4931                    Some((hdr.seq, hdr.ack)),
4932                    (flags & SIDE_TRANSPORT_FLAG_COMPACT_RELIABLE_HEADER) != 0,
4933                    rel_off + rel_len,
4934                )
4935            } else {
4936                (None, None, false, off)
4937            };
4938        if payload_off > data.len() {
4939            return Err(TelemetryError::Unpack("short buffer"));
4940        }
4941        let payload = &data[payload_off..];
4942        let prefix = Arc::<[u8]>::from(&data[1..data_size_off]);
4943        let between_end = reliable_span
4944            .map(|(rel_off, _, _)| rel_off)
4945            .unwrap_or(payload_off);
4946        let between = Arc::<[u8]>::from(&data[between_start..between_end]);
4947        let base_flags =
4948            flags & !(SIDE_TRANSPORT_FLAG_PAYLOAD_COMPRESSED | SIDE_TRANSPORT_FLAG_PACKET_NONCE);
4949        let mut hash = 0xD1B5_4A32_9C7E_01F3u64;
4950        hash = hash_bytes_u64(hash, &[base_flags]);
4951        hash = hash_bytes_u64(hash, &prefix);
4952        hash = hash_bytes_u64(hash, &between);
4953        if let Some(rel_flags) = reliable_flags {
4954            hash = hash_bytes_u64(hash, &[rel_flags]);
4955        }
4956        let template = SideHeaderTemplate {
4957            hash,
4958            base_flags,
4959            prefix,
4960            between,
4961            reliable_flags,
4962            reliable_compact,
4963        };
4964        Ok((
4965            template,
4966            ty,
4967            flags,
4968            data_size,
4969            timestamp,
4970            nonce,
4971            reliable_seq_ack,
4972            payload,
4973        ))
4974    }
4975
4976    fn reconstruct_side_compact_frame(
4977        template: &SideHeaderTemplate,
4978        body: &[u8],
4979        timestamp_mode: SideCompactTimestampMode,
4980        timestamp_base: Option<u64>,
4981    ) -> TelemetryResult<(Arc<[u8]>, u64)> {
4982        if body.is_empty() {
4983            return Err(TelemetryError::Unpack("short side compact frame"));
4984        }
4985        let mut off = 0usize;
4986        let flags = body[off];
4987        off += 1;
4988        if (flags & !(SIDE_TRANSPORT_FLAG_PAYLOAD_COMPRESSED | SIDE_TRANSPORT_FLAG_PACKET_NONCE))
4989            != template.base_flags
4990        {
4991            return Err(TelemetryError::Unpack("side compact flags mismatch"));
4992        }
4993        let data_size = Self::read_uleb128_local(body, &mut off)?;
4994        let timestamp = match timestamp_mode {
4995            SideCompactTimestampMode::Absolute => Self::read_uleb128_local(body, &mut off)?,
4996            SideCompactTimestampMode::Delta => {
4997                let timestamp_field = Self::read_uleb128_local(body, &mut off)?;
4998                let base = timestamp_base.ok_or(TelemetryError::Unpack(
4999                    "missing side compact timestamp context",
5000                ))?;
5001                base.checked_add(timestamp_field)
5002                    .ok_or(TelemetryError::Unpack(
5003                        "side compact timestamp delta overflow",
5004                    ))?
5005            }
5006            SideCompactTimestampMode::Omitted => timestamp_base.ok_or(TelemetryError::Unpack(
5007                "missing side compact timestamp context",
5008            ))?,
5009        };
5010        let nonce = if (flags & SIDE_TRANSPORT_FLAG_PACKET_NONCE) != 0 {
5011            Some(Self::read_uleb128_local(body, &mut off)?)
5012        } else {
5013            None
5014        };
5015        let reliable_seq_ack = if template.reliable_flags.is_some() {
5016            let seq = u32::try_from(Self::read_uleb128_local(body, &mut off)?)
5017                .map_err(|_| TelemetryError::Unpack("side compact reliable seq too large"))?;
5018            let ack = u32::try_from(Self::read_uleb128_local(body, &mut off)?)
5019                .map_err(|_| TelemetryError::Unpack("side compact reliable ack too large"))?;
5020            Some((seq, ack))
5021        } else {
5022            None
5023        };
5024        let payload = &body[off..];
5025        let mut raw = Vec::with_capacity(
5026            1 + template.prefix.len() + template.between.len() + payload.len() + 32,
5027        );
5028        raw.push(flags);
5029        raw.extend_from_slice(&template.prefix);
5030        Self::write_uleb128_local(data_size, &mut raw);
5031        Self::write_uleb128_local(timestamp, &mut raw);
5032        if let Some(nonce) = nonce {
5033            Self::write_uleb128_local(nonce, &mut raw);
5034        }
5035        raw.extend_from_slice(&template.between);
5036        if let Some(rel_flags) = template.reliable_flags {
5037            let (seq, ack) =
5038                reliable_seq_ack.ok_or(TelemetryError::Unpack("missing side compact reliable"))?;
5039            wire_format::write_reliable_header_encoded(
5040                wire_format::ReliableHeader {
5041                    flags: rel_flags,
5042                    seq,
5043                    ack,
5044                },
5045                template.reliable_compact,
5046                &mut raw,
5047            );
5048        }
5049        raw.extend_from_slice(payload);
5050        let crc = Self::crc32_bytes(&raw);
5051        raw.extend_from_slice(&crc.to_le_bytes());
5052        Ok((Arc::from(raw), timestamp))
5053    }
5054
5055    fn split_side_transport_frame(
5056        &self,
5057        side: RelaySideId,
5058        frame: Arc<[u8]>,
5059        max_frame_bytes: usize,
5060    ) -> TelemetryResult<Vec<Arc<[u8]>>> {
5061        if max_frame_bytes <= SIDE_TRANSPORT_CHUNK_OVERHEAD {
5062            return Err(TelemetryError::BadArg);
5063        }
5064        let payload_budget = max_frame_bytes - SIDE_TRANSPORT_CHUNK_OVERHEAD;
5065        let mut st = self.state.lock();
5066        let side_state = st
5067            .side_transport
5068            .get_mut(&side)
5069            .ok_or(TelemetryError::BadArg)?;
5070        let transfer_id = side_state.next_chunk_id.wrapping_add(1).max(1);
5071        side_state.next_chunk_id = transfer_id;
5072        drop(st);
5073
5074        let total = frame.len().div_ceil(payload_budget);
5075        let total_u16 =
5076            u16::try_from(total).map_err(|_| TelemetryError::PacketTooLarge("too many chunks"))?;
5077        let mut frames = Vec::with_capacity(total);
5078        for (idx, chunk) in frame.chunks(payload_budget).enumerate() {
5079            let mut body = Vec::with_capacity(8 + chunk.len());
5080            body.extend_from_slice(&transfer_id.to_le_bytes());
5081            body.extend_from_slice(&(idx as u16).to_le_bytes());
5082            body.extend_from_slice(&total_u16.to_le_bytes());
5083            body.extend_from_slice(chunk);
5084            frames.push(Self::wrap_side_transport_frame(
5085                SIDE_TRANSPORT_KIND_CHUNK,
5086                &body,
5087            ));
5088        }
5089        Ok(frames)
5090    }
5091
5092    fn encode_side_transport_frames(
5093        &self,
5094        side: RelaySideId,
5095        opts: RelaySideOptions,
5096        raw: Arc<[u8]>,
5097    ) -> TelemetryResult<Vec<Arc<[u8]>>> {
5098        if !opts.header_template_enabled && opts.max_frame_bytes == 0 {
5099            return Ok(vec![raw]);
5100        }
5101        let raw_len = raw.len();
5102        let mut compact_payload_len = None;
5103        let mut used_compact = false;
5104        let mut used_timestamp_delta = false;
5105        let mut omitted_timestamp = false;
5106        let (template, ty, flags, data_size, timestamp, nonce, reliable_seq_ack, payload) =
5107            Self::extract_side_header_template(raw.as_ref())?;
5108        let (template_id, use_compact, previous_timestamp) = {
5109            let mut st = self.state.lock();
5110            let side_state = st
5111                .side_transport
5112                .get_mut(&side)
5113                .ok_or(TelemetryError::BadArg)?;
5114            if let Some(id) = side_state.tx_template_ids.get(&template.hash).copied() {
5115                let previous = side_state.tx_last_timestamps.get(&id).copied();
5116                const FULL_REFRESH_AFTER_COMPACT_FRAMES: u8 = 8;
5117                let compact_uses = side_state.tx_compact_uses.entry(id).or_default();
5118                let use_compact = *compact_uses < FULL_REFRESH_AFTER_COMPACT_FRAMES;
5119                if use_compact {
5120                    *compact_uses = compact_uses.saturating_add(1);
5121                } else {
5122                    *compact_uses = 0;
5123                }
5124                (id, use_compact, previous)
5125            } else {
5126                let next = side_state.next_template_id.wrapping_add(1).max(1);
5127                side_state.next_template_id = next;
5128                let evicted = side_state.insert_tx_template(
5129                    template,
5130                    next,
5131                    opts.max_side_transport_templates,
5132                );
5133                if evicted {
5134                    st.side_runtime_stats
5135                        .entry(side)
5136                        .or_default()
5137                        .note_side_transport_template_eviction();
5138                }
5139                if let Some(side_state) = st.side_transport.get_mut(&side) {
5140                    side_state.tx_last_timestamps.insert(next, timestamp);
5141                }
5142                (next, false, None)
5143            }
5144        };
5145        let wrapped = if use_compact {
5146            used_compact = true;
5147            compact_payload_len = Some(payload.len());
5148            let timestamp_field = if let Some(previous) = previous_timestamp {
5149                let delta = timestamp.saturating_sub(previous);
5150                let omit_timestamp = opts.omit_unchanged_compact_timestamps
5151                    || opts.compact_timestamp_omission_types.contains(ty);
5152                if omit_timestamp && timestamp == previous {
5153                    omitted_timestamp = true;
5154                    None
5155                } else if timestamp >= previous
5156                    && Self::uleb128_len_local(delta) < Self::uleb128_len_local(timestamp)
5157                {
5158                    used_timestamp_delta = true;
5159                    Some(delta)
5160                } else {
5161                    Some(timestamp)
5162                }
5163            } else {
5164                Some(timestamp)
5165            };
5166            let mut body = Vec::with_capacity(payload.len() + 32);
5167            body.push(flags);
5168            Self::write_uleb128_local(u64::from(template_id), &mut body);
5169            Self::write_uleb128_local(data_size, &mut body);
5170            if let Some(timestamp_field) = timestamp_field {
5171                Self::write_uleb128_local(timestamp_field, &mut body);
5172            }
5173            if (flags & SIDE_TRANSPORT_FLAG_PACKET_NONCE) != 0 {
5174                Self::write_uleb128_local(u64::from(nonce), &mut body);
5175            }
5176            if let Some((seq, ack)) = reliable_seq_ack {
5177                Self::write_uleb128_local(u64::from(seq), &mut body);
5178                Self::write_uleb128_local(u64::from(ack), &mut body);
5179            }
5180            body.extend_from_slice(payload);
5181            {
5182                let mut st = self.state.lock();
5183                if let Some(side_state) = st.side_transport.get_mut(&side) {
5184                    side_state.tx_last_timestamps.insert(template_id, timestamp);
5185                }
5186            }
5187            let kind = if omitted_timestamp {
5188                SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP
5189            } else if used_timestamp_delta {
5190                SIDE_TRANSPORT_KIND_COMPACT_DELTA
5191            } else {
5192                SIDE_TRANSPORT_KIND_COMPACT
5193            };
5194            Self::wrap_side_transport_frame(kind, &body)
5195        } else {
5196            let mut body = Vec::with_capacity(raw.len() + 4);
5197            Self::write_uleb128_local(u64::from(template_id), &mut body);
5198            body.extend_from_slice(raw.as_ref());
5199            Self::wrap_side_transport_frame(SIDE_TRANSPORT_KIND_FULL, &body)
5200        };
5201        let frames = if opts.max_frame_bytes != 0 && wrapped.len() > opts.max_frame_bytes {
5202            self.split_side_transport_frame(side, wrapped, opts.max_frame_bytes)
5203        } else {
5204            Ok(vec![wrapped])
5205        }?;
5206        let wire_len = frames.iter().map(|frame| frame.len()).sum::<usize>();
5207        let mut st = self.state.lock();
5208        let stats = st.side_runtime_stats.entry(side).or_default();
5209        if used_compact {
5210            let overhead = compact_payload_len
5211                .map(|payload_len| wire_len.saturating_sub(payload_len))
5212                .unwrap_or(wire_len);
5213            stats.note_side_transport_compact(
5214                raw_len,
5215                wire_len,
5216                overhead,
5217                used_timestamp_delta,
5218                omitted_timestamp,
5219            );
5220            if opts.compact_header_target_bytes != 0 && overhead > opts.compact_header_target_bytes
5221            {
5222                stats.note_side_transport_compact_target_miss();
5223            }
5224        } else {
5225            stats.note_side_transport_full(raw_len, wire_len);
5226        }
5227        if frames.len() > 1 {
5228            stats.note_side_transport_chunks(frames.len());
5229        }
5230        Ok(frames)
5231    }
5232
5233    fn decode_side_transport_frame(
5234        &self,
5235        side: RelaySideId,
5236        bytes: &[u8],
5237    ) -> TelemetryResult<Option<Arc<[u8]>>> {
5238        let Some((kind, body)) = Self::parse_side_transport_wrapper(bytes)? else {
5239            return Ok(Some(Arc::from(bytes)));
5240        };
5241        match kind {
5242            SIDE_TRANSPORT_KIND_FULL => {
5243                let mut off = 0usize;
5244                let template_id = u32::try_from(Self::read_uleb128_local(body, &mut off)?)
5245                    .map_err(|_| TelemetryError::Unpack("side template id too large"))?;
5246                let raw = Arc::<[u8]>::from(&body[off..]);
5247                if let Ok((template, _, _, _, timestamp, _, _, _)) =
5248                    Self::extract_side_header_template(raw.as_ref())
5249                {
5250                    let mut st = self.state.lock();
5251                    let max_templates = st
5252                        .sides
5253                        .get(side)
5254                        .and_then(|side| side.as_ref())
5255                        .map(|side| side.opts.max_side_transport_templates)
5256                        .unwrap_or(DEFAULT_SIDE_TRANSPORT_TEMPLATE_LIMIT);
5257                    let evicted = st.side_transport.get_mut(&side).is_some_and(|side_state| {
5258                        let evicted =
5259                            side_state.insert_rx_template(template_id, template, max_templates);
5260                        side_state.rx_last_timestamps.insert(template_id, timestamp);
5261                        evicted
5262                    });
5263                    if evicted {
5264                        st.side_runtime_stats
5265                            .entry(side)
5266                            .or_default()
5267                            .note_side_transport_template_eviction();
5268                    }
5269                }
5270                Ok(Some(raw))
5271            }
5272            SIDE_TRANSPORT_KIND_COMPACT
5273            | SIDE_TRANSPORT_KIND_COMPACT_DELTA
5274            | SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP => {
5275                if body.is_empty() {
5276                    return Err(TelemetryError::Unpack("short side compact frame"));
5277                }
5278                let mut off = 1usize;
5279                let template_id = u32::try_from(Self::read_uleb128_local(body, &mut off)?)
5280                    .map_err(|_| TelemetryError::Unpack("side template id too large"))?;
5281                let mut compact_body = Vec::with_capacity(1 + body.len().saturating_sub(off));
5282                compact_body.push(body[0]);
5283                compact_body.extend_from_slice(&body[off..]);
5284                let (template, timestamp_base) = {
5285                    let st = self.state.lock();
5286                    let state = st.side_transport.get(&side);
5287                    let template = state
5288                        .and_then(|state| state.rx_templates_by_id.get(&template_id))
5289                        .cloned();
5290                    let timestamp_base = if matches!(
5291                        kind,
5292                        SIDE_TRANSPORT_KIND_COMPACT_DELTA
5293                            | SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP
5294                    ) {
5295                        state
5296                            .and_then(|state| state.rx_last_timestamps.get(&template_id))
5297                            .copied()
5298                    } else {
5299                        None
5300                    };
5301                    (template, timestamp_base)
5302                };
5303                let template =
5304                    template.ok_or(TelemetryError::Unpack("unknown side compact template"))?;
5305                let timestamp_mode = match kind {
5306                    SIDE_TRANSPORT_KIND_COMPACT_DELTA => SideCompactTimestampMode::Delta,
5307                    SIDE_TRANSPORT_KIND_COMPACT_SAME_TIMESTAMP => SideCompactTimestampMode::Omitted,
5308                    _ => SideCompactTimestampMode::Absolute,
5309                };
5310                let (frame, timestamp) = Self::reconstruct_side_compact_frame(
5311                    &template,
5312                    &compact_body,
5313                    timestamp_mode,
5314                    timestamp_base,
5315                )?;
5316                let mut st = self.state.lock();
5317                if let Some(side_state) = st.side_transport.get_mut(&side) {
5318                    side_state.rx_last_timestamps.insert(template_id, timestamp);
5319                }
5320                Ok(Some(frame))
5321            }
5322            SIDE_TRANSPORT_KIND_CHUNK => {
5323                if body.len() < 8 {
5324                    return Err(TelemetryError::Unpack("short side chunk frame"));
5325                }
5326                let transfer_id = u32::from_le_bytes([body[0], body[1], body[2], body[3]]);
5327                let index = u16::from_le_bytes([body[4], body[5]]);
5328                let total = u16::from_le_bytes([body[6], body[7]]);
5329                let payload = Arc::<[u8]>::from(&body[8..]);
5330                let assembled = {
5331                    let mut st = self.state.lock();
5332                    let side_state = st
5333                        .side_transport
5334                        .get_mut(&side)
5335                        .ok_or(TelemetryError::BadArg)?;
5336                    let entry = side_state.rx_chunks.entry(transfer_id).or_default();
5337                    if entry.total == 0 {
5338                        entry.total = total;
5339                    } else if entry.total != total {
5340                        side_state.rx_chunks.remove(&transfer_id);
5341                        return Err(TelemetryError::Unpack("side chunk total mismatch"));
5342                    }
5343                    entry.received.entry(index).or_insert(payload);
5344                    if entry.received.len() == usize::from(total) {
5345                        let entry = side_state
5346                            .rx_chunks
5347                            .remove(&transfer_id)
5348                            .ok_or(TelemetryError::Unpack("side chunk missing"))?;
5349                        let mut out = Vec::new();
5350                        for idx in 0..entry.total {
5351                            let chunk = entry
5352                                .received
5353                                .get(&idx)
5354                                .ok_or(TelemetryError::Unpack("side chunk gap"))?;
5355                            out.extend_from_slice(chunk);
5356                        }
5357                        Some(Arc::<[u8]>::from(out))
5358                    } else {
5359                        None
5360                    }
5361                };
5362                match assembled {
5363                    Some(frame) => self.decode_side_transport_frame(side, frame.as_ref()),
5364                    None => Ok(None),
5365                }
5366            }
5367            _ => Err(TelemetryError::Unpack("unknown side transport frame")),
5368        }
5369    }
5370
5371    /// Helper: call a TX handler with the best representation we have.
5372    /// - Packet handler + Packet item: direct.
5373    /// - Packed handler + Packed item: direct.
5374    /// - Packet handler + Packed item: unpack for this call.
5375    /// - Packed handler + Packet item: pack for this call.
5376    fn call_tx_handler(
5377        &self,
5378        side: RelaySideId,
5379        handler: &RelayTxHandlerFn,
5380        data: &RelayItem,
5381    ) -> TelemetryResult<()> {
5382        let opts = {
5383            let st = self.state.lock();
5384            Self::side_ref(&st, side)?.opts
5385        };
5386        let Some(_side_tx_guard) = self.try_enter_side_tx() else {
5387            return Err(TelemetryError::Io("side tx busy"));
5388        };
5389        let started_ms = self.clock.now_ms();
5390        let ty = match data {
5391            RelayItem::Packet(pkt) => pkt.data_type(),
5392            RelayItem::Packed(bytes) => wire_format::peek_envelope(bytes.as_ref())?.ty,
5393        };
5394        let result = match (handler, data) {
5395            // Fast paths
5396            (RelayTxHandlerFn::Packed(f), RelayItem::Packed(bytes)) => {
5397                let frames = self.encode_side_transport_frames(side, opts, bytes.clone())?;
5398                let mut sent_bytes = 0usize;
5399                for frame in frames {
5400                    f(frame.as_ref())?;
5401                    sent_bytes = sent_bytes.saturating_add(frame.len());
5402                }
5403                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
5404                self.note_side_tx_success(side, ty, sent_bytes, 1);
5405                return Ok(());
5406            }
5407            (RelayTxHandlerFn::Packet(f), RelayItem::Packet(pkt)) => f(pkt),
5408
5409            // Conversion paths
5410            (RelayTxHandlerFn::Packed(f), RelayItem::Packet(pkt)) => {
5411                let owned = wire_format::pack_packet(pkt);
5412                let frames = self.encode_side_transport_frames(side, opts, owned)?;
5413                let mut sent_bytes = 0usize;
5414                for frame in frames {
5415                    f(frame.as_ref())?;
5416                    sent_bytes = sent_bytes.saturating_add(frame.len());
5417                }
5418                self.record_side_tx_sample(side, sent_bytes, started_ms, self.clock.now_ms());
5419                self.note_side_tx_success(side, ty, sent_bytes, 1);
5420                return Ok(());
5421            }
5422            (RelayTxHandlerFn::Packet(f), RelayItem::Packed(bytes)) => {
5423                if wire_format::peek_frame_info(bytes.as_ref())
5424                    .ok()
5425                    .is_some_and(|frame| frame.ack_only())
5426                {
5427                    return Ok(());
5428                }
5429                let pkt = wire_format::unpack_packet(bytes.as_ref())?;
5430                f(&pkt)
5431            }
5432        };
5433        if result.is_ok()
5434            && let Ok(bytes) = Self::relay_item_wire_len(data)
5435        {
5436            self.record_side_tx_sample(side, bytes, started_ms, self.clock.now_ms());
5437            self.note_side_tx_success(side, ty, bytes, 1);
5438        } else if result.is_err() {
5439            self.note_side_tx_failure(side, ty, 1);
5440        }
5441        result
5442    }
5443
5444    fn adjust_reliable_for_side(
5445        &self,
5446        opts: RelaySideOptions,
5447        data: RelayItem,
5448    ) -> TelemetryResult<Option<RelayItem>> {
5449        if opts.reliable_enabled {
5450            return Ok(Some(data));
5451        }
5452
5453        match data {
5454            RelayItem::Packed(bytes) => {
5455                let frame = match wire_format::peek_frame_info(bytes.as_ref()) {
5456                    Ok(frame) => frame,
5457                    Err(_) => return Ok(Some(RelayItem::Packed(bytes))),
5458                };
5459                if is_reliable_type(frame.envelope.ty)
5460                    && let Some(hdr) = frame.reliable
5461                {
5462                    if (hdr.flags & wire_format::RELIABLE_FLAG_ACK_ONLY) != 0 {
5463                        return Ok(None);
5464                    }
5465                    if (hdr.flags & wire_format::RELIABLE_FLAG_UNSEQUENCED) == 0 {
5466                        let Some(rewritten) = wire_format::rewrite_reliable_header_owned(
5467                            bytes.as_ref(),
5468                            wire_format::RELIABLE_FLAG_UNSEQUENCED,
5469                            hdr.seq,
5470                            0,
5471                        )?
5472                        else {
5473                            return Ok(Some(RelayItem::Packed(bytes)));
5474                        };
5475                        return Ok(Some(RelayItem::Packed(rewritten)));
5476                    }
5477                }
5478                Ok(Some(RelayItem::Packed(bytes)))
5479            }
5480            RelayItem::Packet(pkt) => {
5481                if matches!(
5482                    pkt.data_type(),
5483                    crate::DataType::ReliableAck
5484                        | crate::DataType::ReliablePartialAck
5485                        | crate::DataType::ReliablePacketRequest
5486                ) {
5487                    return Ok(None);
5488                }
5489                Ok(Some(RelayItem::Packet(pkt)))
5490            }
5491        }
5492    }
5493
5494    /// Drain the RX queue fully, expanding to TX items.
5495    #[inline]
5496    pub fn process_rx_queue(&self) -> TelemetryResult<()> {
5497        self.process_rx_queue_with_timeout(0)
5498    }
5499
5500    /// Drain the TX queue fully, invoking per-side TX handlers.
5501    ///
5502    /// If called from inside a side TX callback, this becomes a no-op so relay TX handlers cannot
5503    /// recurse into nested queue drains on the same stack.
5504    #[inline]
5505    pub fn process_tx_queue(&self) -> TelemetryResult<()> {
5506        self.process_tx_queue_with_timeout(0)
5507    }
5508
5509    /// Drain RX then TX queues fully (one pass).
5510    #[inline]
5511    pub fn process_all_queues(&self) -> TelemetryResult<()> {
5512        self.process_all_queues_with_timeout(0)
5513    }
5514
5515    /// Process the TX queue for up to `timeout_ms` milliseconds.
5516    ///
5517    /// `timeout_ms == 0` drains fully. If called from inside a side TX callback, this becomes a
5518    /// no-op so relay TX handlers cannot recurse into nested queue drains on the same stack.
5519    pub fn process_tx_queue_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
5520        if self.side_tx_active() {
5521            return Ok(());
5522        }
5523        #[cfg(feature = "discovery")]
5524        {
5525            let _ = self.poll_discovery()?;
5526        }
5527        let start = self.clock.now_ms();
5528        loop {
5529            self.process_reliable_timeouts()?;
5530            if self.process_replay_queue_item()? {
5531                if timeout_ms != 0 && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
5532                    break;
5533                }
5534                continue;
5535            }
5536            let Some((src, dst, handler, opts, data)) = self.pop_ready_tx_item() else {
5537                break;
5538            };
5539            match self.send_tx_item(src, dst, handler, opts, data.clone()) {
5540                Ok(sent) => {
5541                    if sent
5542                        && timeout_ms != 0
5543                        && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64
5544                    {
5545                        break;
5546                    }
5547                }
5548                Err(e) if Self::is_side_tx_busy(&e) => {
5549                    let priority = Self::relay_item_priority(&data)?;
5550                    let mut st = self.state.lock();
5551                    st.push_tx(RelayTxItem {
5552                        src,
5553                        dst,
5554                        data,
5555                        priority,
5556                    })?;
5557                    break;
5558                }
5559                Err(e) => return Err(e),
5560            }
5561        }
5562        Ok(())
5563    }
5564
5565    /// Process RX queue with timeout.
5566    pub fn process_rx_queue_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
5567        #[cfg(feature = "discovery")]
5568        {
5569            let _ = self.poll_discovery()?;
5570        }
5571        let start = self.clock.now_ms();
5572        loop {
5573            let item_opt = {
5574                let mut st = self.state.lock();
5575                st.rx_queue.pop_front()
5576            };
5577            let Some(item) = item_opt else { break };
5578            self.process_rx_queue_item(item)?;
5579
5580            if timeout_ms != 0 && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
5581                break;
5582            }
5583        }
5584        Ok(())
5585    }
5586
5587    /// Process RX and TX queues interleaved for up to `timeout_ms` milliseconds.
5588    ///
5589    /// `timeout_ms == 0` drains fully. If called from inside a side TX callback, this becomes a
5590    /// no-op so relay TX handlers cannot recurse into nested queue drains on the same stack.
5591    pub fn process_all_queues_with_timeout(&self, timeout_ms: u32) -> TelemetryResult<()> {
5592        if self.side_tx_active() {
5593            return Ok(());
5594        }
5595        #[cfg(feature = "discovery")]
5596        {
5597            let _ = self.poll_discovery()?;
5598        }
5599        let drain_fully = timeout_ms == 0;
5600        let start = if drain_fully { 0 } else { self.clock.now_ms() };
5601
5602        loop {
5603            let mut did_any = false;
5604            self.process_reliable_timeouts()?;
5605
5606            // First move RX → TX
5607            if let Some(item) = {
5608                let mut st = self.state.lock();
5609                st.rx_queue.pop_front()
5610            } {
5611                self.process_rx_queue_item(item)?;
5612                did_any = true;
5613            }
5614
5615            if !drain_fully && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
5616                break;
5617            }
5618
5619            if self.process_replay_queue_item()? {
5620                did_any = true;
5621            }
5622
5623            // Then send out TX
5624            let sent_one = if let Some((src, dst, handler, opts, data)) = self.pop_ready_tx_item() {
5625                self.send_tx_item(src, dst, handler, opts, data)?
5626            } else {
5627                false
5628            };
5629
5630            if sent_one {
5631                did_any = true;
5632            }
5633
5634            if !drain_fully && self.clock.now_ms().wrapping_sub(start) >= timeout_ms as u64 {
5635                break;
5636            }
5637
5638            if !did_any {
5639                break;
5640            }
5641        }
5642
5643        Ok(())
5644    }
5645
5646    /// Runs one application-loop maintenance cycle.
5647    ///
5648    /// This polls built-in discovery when that feature is compiled in, then drains queued RX/TX
5649    /// work for up to `timeout_ms` milliseconds.
5650    pub fn periodic(&self, timeout_ms: u32) -> TelemetryResult<()> {
5651        #[cfg(feature = "discovery")]
5652        {
5653            let _ = self.poll_discovery()?;
5654        }
5655
5656        self.process_all_queues_with_timeout(timeout_ms)
5657    }
5658}