1use super::coordination::room::{GatewayRequiredRoomBackend, RoomBackend};
2use crate::signaling::{GatewayRequiredSignalingBackend, SignalingBackend};
3use iroh::Endpoint;
4use iroh_tickets::endpoint::EndpointTicket;
5use std::collections::{HashMap, HashSet};
6use std::str::FromStr;
7#[cfg(not(target_arch = "wasm32"))]
8use std::sync::atomic::Ordering;
9use std::sync::atomic::{AtomicBool, AtomicU64};
10use std::sync::{Arc, Mutex, RwLock as StdRwLock};
11use tokio::sync::RwLock;
12
13#[cfg(not(target_arch = "wasm32"))]
14use crate::client::scope_classifier::{default_scope_classifier, ScopeClassifier};
15
16#[cfg(target_arch = "wasm32")]
17fn wasm_init_log(stage: &str) {
18 let now = js_sys::Date::now();
19 web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
20 "[OPENRTC][WASM-INIT] ts_ms={:.0} stage={}",
21 now, stage
22 )));
23}
24
25#[cfg(target_arch = "wasm32")]
26use crate::wasm_node::{AcceptEvent, ConnectEvent, IrohWasmNode};
27
28#[cfg(not(target_arch = "wasm32"))]
29use crate::native_node::{AcceptEvent, ConnectEvent, IncomingStream, IrohNativeNode};
30
31#[cfg(not(target_arch = "wasm32"))]
32#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct EndpointHandle {
35 pub node_id: String,
36 pub node_addr: String,
37}
38
39#[cfg(not(target_arch = "wasm32"))]
40pub struct BiStream {
41 pub send: crate::application_crypto_streams::PeerSendStream,
42 pub recv: crate::application_crypto_streams::PeerRecvStream,
43 pub id: String,
44}
45
46#[cfg(not(target_arch = "wasm32"))]
50pub enum IncomingBiStreamDisposition {
51 Consumed,
52 Forward {
53 send: iroh::endpoint::SendStream,
54 recv: iroh::endpoint::RecvStream,
55 recv_prefix: Vec<u8>,
60 },
61}
62
63#[cfg(not(target_arch = "wasm32"))]
64#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct NativePeerDataEvent {
67 pub connection_id: String,
68 pub remote_node_id: Option<String>,
69 pub transport: String,
70 pub transport_stable_id: u64,
71 pub transport_generation: u64,
72 pub route_generation: u64,
73 pub payload: Vec<u8>,
74}
75
76#[cfg(not(target_arch = "wasm32"))]
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub(crate) struct NativePeerDataGeneration {
79 pub transport_stable_id: u64,
80 pub transport_generation: u64,
81 pub route_generation: u64,
82}
83
84#[cfg(not(target_arch = "wasm32"))]
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub(crate) struct NativeBleUpgradeAttemptState {
87 pub generation: NativePeerDataGeneration,
88 pub attempts: u8,
89}
90
91#[cfg(not(target_arch = "wasm32"))]
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub(crate) struct NativeControlStreamOwner {
97 pub transport_stable_id: u64,
98 pub stream_rank: u64,
99}
100
101#[cfg(not(target_arch = "wasm32"))]
102#[derive(Clone)]
103pub(crate) struct NativeControlStreamEntry {
104 pub owner: NativeControlStreamOwner,
105 pub endpoint_id: iroh::EndpointId,
106 pub send: Arc<tokio::sync::Mutex<iroh::endpoint::SendStream>>,
107}
108
109#[cfg(not(target_arch = "wasm32"))]
118#[derive(Debug, Clone, Copy)]
119pub(crate) struct NativeTransportProtocolActivity {
120 pub transport_stable_id: u64,
121 pub observed_at: std::time::Instant,
122}
123
124#[cfg(not(target_arch = "wasm32"))]
125#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
126pub(crate) struct NativeAdmissionStreamContracts {
127 pub inbound: Option<crate::native_protocol::SessionTokenStreamContract>,
128 pub outbound: Option<crate::native_protocol::SessionTokenStreamContract>,
129}
130
131#[cfg(not(target_arch = "wasm32"))]
132#[derive(Debug, Clone)]
133pub(crate) struct CachedManagedScopeTicket {
134 pub scope: crate::session_token::GrantScope,
135 pub token: String,
136 pub max_connections: u32,
137 pub compound_ticket: String,
138 pub iroh_ticket: String,
139}
140
141#[cfg(not(target_arch = "wasm32"))]
142#[derive(Debug, Clone)]
143pub(crate) struct NativeRouteRepairCredential {
144 pub token: String,
145 pub token_payload: String,
146 pub authoritative_device_id: String,
147}
148
149#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
150#[derive(Debug, Clone)]
151pub(crate) struct NativeWebRTCSuppression {
152 pub until_ms: i64,
153 pub reason: String,
154}
155
156#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
161#[derive(Debug, Clone)]
162pub(crate) struct NativeWebRTCAttemptInFlight {
163 pub attempt_id: String,
166 pub negotiation_id: String,
167 pub transport_stable_id: Option<u64>,
169 pub started_at_ms: i64,
170 pub expires_at_ms: i64,
171 pub preserve_on_direct_path: bool,
174 pub last_skip_log_ms: i64,
178}
179
180#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub(crate) struct DeferredManagedRetirement {
190 pub reason: Option<String>,
191 pub transport_stable_id: Option<u64>,
192 pub transport_generation: u64,
193}
194
195#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
196#[derive(Debug, Clone, PartialEq, Eq)]
197pub(crate) struct NativeMoQRouteProofState {
198 pub route_instance_id: usize,
199 pub probe_id: String,
200 pub local_node_id: String,
201 pub remote_node_id: String,
202 pub retry_active: bool,
203 pub proven: bool,
204}
205
206#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub(crate) struct NativeWebRTCRouteProofState {
209 pub route_instance_id: usize,
210 pub probe_id: String,
211 pub negotiation_id: String,
212 pub retry_active: bool,
213 pub proven: bool,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
221#[cfg(feature = "legacy-v1")]
222pub struct NativeTrustToken {
223 pub token: String,
224 pub expires_at_ms: u64,
225}
226
227#[allow(dead_code)]
228#[derive(Clone)]
229pub struct Client {
230 app_tag: String,
231 signaling: Arc<dyn SignalingBackend>,
232 pub room: Arc<dyn RoomBackend>,
233 node_id: Arc<RwLock<Option<String>>>,
234 pub iroh_endpoint: Arc<RwLock<Option<Endpoint>>>,
235 #[cfg(target_arch = "wasm32")]
236 pub iroh_node: Arc<RwLock<Option<IrohWasmNode>>>,
237 #[cfg(not(target_arch = "wasm32"))]
238 pub iroh_node: Arc<RwLock<Option<IrohNativeNode>>>,
239 #[cfg(not(target_arch = "wasm32"))]
240 native_application_streams: async_channel::Sender<IncomingStream>,
241 #[cfg(not(target_arch = "wasm32"))]
242 native_application_streams_receiver: async_channel::Receiver<IncomingStream>,
243 pub connection_manager: Arc<crate::connection_manager::ConnectionManager>,
244 #[cfg(not(target_arch = "wasm32"))]
245 native_device_identity: Arc<RwLock<Option<crate::native_device::NativeDeviceIdentity>>>,
246 #[cfg(not(target_arch = "wasm32"))]
247 native_device_base_dir: Arc<RwLock<Option<std::path::PathBuf>>>,
248 #[cfg(not(target_arch = "wasm32"))]
249 native_device_identity_init_guard: Arc<tokio::sync::Mutex<()>>,
250 #[cfg(not(target_arch = "wasm32"))]
251 native_device_updates:
252 tokio::sync::broadcast::Sender<crate::native_device::NativeDeviceIdentity>,
253 #[cfg(not(target_arch = "wasm32"))]
254 native_connection_state_updates: tokio::sync::broadcast::Sender<ConnectionStateSnapshot>,
255 #[cfg(not(target_arch = "wasm32"))]
256 native_peer_data_updates: tokio::sync::broadcast::Sender<NativePeerDataEvent>,
257 auto_connect_loop_key: Arc<Mutex<Option<(String, String)>>>,
258 auto_connect_generation: Arc<AtomicU64>,
259 #[cfg(not(target_arch = "wasm32"))]
262 external_desired_peer_actor:
263 Arc<tokio::sync::Mutex<auto_connect_impl::NativeExternalAutoConnectActorState>>,
264 auto_connect_excluded: Arc<Mutex<HashSet<String>>>,
266 auto_connect_peer_requested_excluded: Arc<Mutex<HashSet<String>>>,
273 auto_connect_excluded_node_aliases: Arc<Mutex<HashMap<String, HashSet<String>>>>,
277 pub session_token_registry: Arc<crate::session_token::SessionTokenRegistry>,
281 #[cfg(not(target_arch = "wasm32"))]
285 inbound_session_admission_transport_ids: Arc<StdRwLock<HashMap<String, u64>>>,
286 #[cfg(not(target_arch = "wasm32"))]
290 native_admission_stream_contracts:
291 Arc<StdRwLock<HashMap<String, NativeAdmissionStreamContracts>>>,
292 remote_session_admission_proofs: Arc<StdRwLock<HashMap<String, RemoteSessionAdmissionProof>>>,
296 pending_inline_reciprocal_admissions:
301 Arc<StdRwLock<HashMap<String, PendingInlineReciprocalAdmission>>>,
302 outbound_application_security_epoch_fingerprints: Arc<StdRwLock<HashMap<String, String>>>,
307 #[cfg(not(target_arch = "wasm32"))]
311 pending_reciprocal_session_admission_requests: Arc<StdRwLock<HashSet<String>>>,
312 #[cfg(not(target_arch = "wasm32"))]
316 native_route_repair_credentials: Arc<StdRwLock<HashMap<String, NativeRouteRepairCredential>>>,
317 connection_application_crypto_keys:
318 Arc<StdRwLock<HashMap<String, [u8; crate::application_crypto::APPLICATION_KEY_BYTES]>>>,
319 connection_application_crypto_required: Arc<StdRwLock<HashSet<String>>>,
320 #[cfg(not(target_arch = "wasm32"))]
324 trusted_user_device_application_crypto_required: Arc<AtomicBool>,
325 connection_application_crypto_confirmed: Arc<StdRwLock<HashSet<String>>>,
329 #[cfg(not(target_arch = "wasm32"))]
335 connection_application_crypto_confirmation_updates: Arc<tokio::sync::Notify>,
336 connection_application_crypto_outbound_sequences: Arc<StdRwLock<HashMap<String, u64>>>,
337 connection_application_key_agreements:
338 Arc<StdRwLock<HashMap<String, crate::key_agreement::EphemeralKeyAgreement>>>,
339 #[cfg(not(target_arch = "wasm32"))]
348 managed_scope_tickets: Arc<StdRwLock<HashMap<String, CachedManagedScopeTicket>>>,
349 known_endpoint_addrs: Arc<RwLock<HashMap<String, iroh::EndpointAddr>>>,
355 known_device_ids_by_node: Arc<StdRwLock<HashMap<String, String>>>,
360 app_backgrounded: Arc<AtomicBool>,
364 #[cfg(target_arch = "wasm32")]
365 wasm_accept_bridge_started: Arc<std::sync::atomic::AtomicBool>,
366 #[cfg(target_arch = "wasm32")]
367 last_emitted_connection_states:
368 Arc<Mutex<std::collections::HashMap<String, WasmConnectionStateFingerprint>>>,
369 #[cfg(target_arch = "wasm32")]
370 last_empty_peer_sessions_warning_ms: Arc<AtomicU64>,
371 iroh_init_guard: Arc<tokio::sync::Mutex<()>>,
376 managed_connect_gates: Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>>,
383 #[cfg(not(target_arch = "wasm32"))]
387 iroh_path_watcher_stable_ids: Arc<Mutex<HashSet<u64>>>,
388 #[cfg(not(target_arch = "wasm32"))]
394 native_custom_transport_kinds: Arc<RwLock<HashMap<u64, IrohPathKind>>>,
395 #[cfg(not(target_arch = "wasm32"))]
399 native_transport_upgrade_providers:
400 Arc<RwLock<HashMap<IrohPathKind, Arc<dyn NativeTransportUpgradeProvider>>>>,
401 #[cfg(not(target_arch = "wasm32"))]
404 native_transport_upgrade_gates:
405 Arc<tokio::sync::Mutex<HashMap<(String, IrohPathKind), String>>>,
406 #[cfg(not(target_arch = "wasm32"))]
410 native_ble_upgrade_attempts:
411 Arc<tokio::sync::Mutex<HashMap<String, NativeBleUpgradeAttemptState>>>,
412 #[cfg(not(target_arch = "wasm32"))]
416 native_peer_transport_capabilities:
417 Arc<RwLock<HashMap<String, HashSet<NativePeerTransportCapability>>>>,
418 #[cfg(not(target_arch = "wasm32"))]
422 native_scoped_webrtc_signal_peer_connections: Arc<RwLock<HashSet<String>>>,
423 pub(crate) presence_loop_tx:
424 Arc<Mutex<Option<tokio::sync::mpsc::Sender<crate::presence::PresenceCommand>>>>,
425 #[cfg(not(target_arch = "wasm32"))]
429 pub(crate) project_id: String,
430 #[cfg(not(target_arch = "wasm32"))]
431 pub(crate) token_provider: Arc<dyn Fn() -> Option<String> + Send + Sync>,
432 transport_config: Arc<RwLock<TransportConfig>>,
433 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
434 local_discovery_registry: crate::local_discovery::LocalDiscoveryRegistry,
435 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
436 mdns_address_lookup: Arc<RwLock<Option<iroh_mdns_address_lookup::MdnsAddressLookup>>>,
437 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
438 native_webrtc_sessions:
439 Arc<RwLock<HashMap<String, Arc<crate::transport::NativeWebRTCDataChannel>>>>,
440 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
441 native_webrtc_route_proofs: Arc<RwLock<HashMap<String, NativeWebRTCRouteProofState>>>,
442 #[cfg(not(target_arch = "wasm32"))]
443 native_optional_route_generations: Arc<RwLock<HashMap<String, NativeOptionalRouteGenerations>>>,
444 #[cfg(not(target_arch = "wasm32"))]
445 native_route_start_gates: Arc<route_adapter::NativeRouteStartGateRegistry>,
446 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
447 native_webrtc_suppressions: Arc<RwLock<HashMap<String, NativeWebRTCSuppression>>>,
448 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
449 native_webrtc_attempt_counts: Arc<RwLock<HashMap<String, u32>>>,
450 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
451 native_webrtc_start_gates: Arc<tokio::sync::Mutex<HashMap<String, Arc<tokio::sync::Notify>>>>,
452 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
453 pub(crate) native_webrtc_attempts_in_flight:
454 Arc<RwLock<HashMap<String, NativeWebRTCAttemptInFlight>>>,
455 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
456 native_webrtc_retry_deadlines: Arc<tokio::sync::Mutex<HashMap<String, i64>>>,
457 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
458 deferred_managed_retirements: Arc<RwLock<HashMap<String, DeferredManagedRetirement>>>,
459 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
460 native_moq_sessions: Arc<RwLock<HashMap<String, Arc<crate::transport::NativeMoQSession>>>>,
461 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
462 native_moq_route_proofs: Arc<RwLock<HashMap<String, NativeMoQRouteProofState>>>,
463 #[cfg(not(target_arch = "wasm32"))]
469 pub(crate) native_control_streams:
470 Arc<tokio::sync::Mutex<HashMap<String, NativeControlStreamEntry>>>,
471 #[cfg(not(target_arch = "wasm32"))]
475 native_transport_protocol_activity:
476 Arc<StdRwLock<HashMap<String, NativeTransportProtocolActivity>>>,
477 #[cfg(not(target_arch = "wasm32"))]
485 pending_native_control_handoffs: Arc<StdRwLock<HashMap<String, u64>>>,
486 #[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
490 scoped_connection_actor_registry: Arc<
491 RwLock<Option<Arc<crate::client::scoped_connection_actor::ScopedConnectionActorRegistry>>>,
492 >,
493 #[cfg(not(target_arch = "wasm32"))]
499 auth_readiness: Arc<crate::client::auth_readiness::AuthReadinessStore>,
500 #[cfg(not(target_arch = "wasm32"))]
503 scope_classifier: Arc<RwLock<Arc<dyn ScopeClassifier>>>,
504}
505
506#[derive(Debug, Clone, PartialEq, Eq)]
507struct RemoteSessionAdmissionProof {
508 transport_stable_id: u64,
509 token_fingerprint: String,
510 approval_scope: String,
511}
512
513#[derive(Debug, Clone)]
514struct PendingInlineReciprocalAdmission {
515 connection_id: String,
516 remote_node_id: String,
517 local_device_id: String,
518 remote_device_id: String,
519 transport_stable_id: u64,
520 stream_instance_id: String,
521 presentation_id: String,
522 token: String,
523 token_fingerprint: String,
524 expected_scope: String,
525 inbound_admission_fingerprint: String,
526 inbound_admission_epoch: u64,
527}
528
529fn now_millis_i64() -> i64 {
530 crate::coordination::now_millis_u64().min(i64::MAX as u64) as i64
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
543pub enum IrohPathKind {
544 DirectQuic,
546 DirectLan,
548 Relay,
550 Ble,
552 Unknown,
554}
555
556#[cfg(not(target_arch = "wasm32"))]
557#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
558pub(crate) enum NativePeerTransportCapability {
559 WebRtc,
560 Moq,
561 Ble,
562}
563
564#[cfg(not(target_arch = "wasm32"))]
571#[async_trait::async_trait]
572pub trait NativeTransportUpgradeProvider: std::fmt::Debug + Send + Sync {
573 fn kind(&self) -> IrohPathKind;
574 fn transport_id(&self) -> u64;
575 async fn prepare_endpoint_addr(
576 &self,
577 endpoint_id: iroh::EndpointId,
578 ) -> anyhow::Result<iroh::EndpointAddr>;
579}
580
581impl IrohPathKind {
582 pub fn is_relay_path(self) -> bool {
583 matches!(self, Self::Relay)
584 }
585
586 pub fn transport_label(self) -> &'static str {
587 match self {
588 Self::DirectQuic => crate::transport_label::IROH_QUIC,
589 Self::DirectLan => crate::transport_label::IROH_LAN,
590 Self::Relay => crate::transport_label::IROH_RELAY,
591 Self::Ble => crate::transport_label::BLE,
592 Self::Unknown => crate::transport_label::IROH,
593 }
594 }
595}
596
597#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
599#[serde(rename_all = "camelCase")]
600pub struct IrohLanConfig {
601 #[serde(default = "default_lan_enabled")]
602 pub enabled: bool,
603 #[serde(default = "default_lan_advertise")]
605 pub advertise: bool,
606}
607
608fn default_lan_enabled() -> bool {
609 true
610}
611
612fn default_lan_advertise() -> bool {
613 true
614}
615
616impl Default for IrohLanConfig {
617 fn default() -> Self {
618 Self {
619 enabled: true,
620 advertise: true,
621 }
622 }
623}
624
625#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)]
630#[serde(rename_all = "camelCase")]
631pub struct BleConfig {
632 #[serde(default)]
633 pub enabled: bool,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub connect_timeout_ms: Option<u64>,
636}
637
638pub(crate) const MANAGED_SETTLE_DEADLINE_MS: i64 =
643 crate::runtime_policy::MANAGED_SETTLE_DEADLINE_MS;
644
645#[cfg(not(target_arch = "wasm32"))]
653pub(crate) const SESSION_ADMISSION_TIMEOUT_MS: u64 =
654 crate::runtime_policy::SESSION_ADMISSION_TIMEOUT_MS;
655
656#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
657#[serde(rename_all = "camelCase")]
658pub enum DeviceConnectionStatus {
659 Disconnected,
660 Connecting,
661 Connected,
662 Failed,
663 Closed,
664 Online,
665}
666
667#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
668#[serde(rename_all = "camelCase")]
669pub enum DevicePresenceStatus {
670 Online,
671 Idle,
672 Offline,
673}
674
675#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
676#[serde(rename_all = "camelCase")]
677pub enum ReadinessState {
678 Connecting,
679 TransportOnly,
680 Settling,
681 Routable,
682 AwaitingReplacement,
683 Closed,
684 Failed,
685}
686
687#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
688#[serde(rename_all = "camelCase")]
689pub struct TransportLatencySnapshot {
690 pub webrtc: Option<u64>,
691 pub webrtc_lan: Option<u64>,
692 pub webrtc_turn: Option<u64>,
693 pub iroh: Option<u64>,
694 pub iroh_lan: Option<u64>,
695 pub iroh_relay: Option<u64>,
696 pub ble: Option<u64>,
697 pub moq: Option<u64>,
698}
699
700impl TransportLatencySnapshot {
701 fn set(&mut self, transport: &str, latency_ms: u64) {
702 match transport.trim().to_ascii_lowercase().as_str() {
703 "webrtc-lan" => self.webrtc_lan = Some(latency_ms),
704 "webrtc-turn" => self.webrtc_turn = Some(latency_ms),
705 "webrtc" => self.webrtc = Some(latency_ms),
706 "iroh-lan" => self.iroh_lan = Some(latency_ms),
707 "iroh-relay" => self.iroh_relay = Some(latency_ms),
708 "iroh" | "iroh-quic" => self.iroh = Some(latency_ms),
709 "ble" => self.ble = Some(latency_ms),
710 "moq" => self.moq = Some(latency_ms),
711 _ => {}
712 }
713 }
714
715 fn get(&self, transport: &str) -> Option<u64> {
716 match transport.trim().to_ascii_lowercase().as_str() {
717 "webrtc-lan" => self.webrtc_lan,
718 "webrtc-turn" => self.webrtc_turn,
719 "webrtc" => self.webrtc,
720 "iroh-lan" => self.iroh_lan,
721 "iroh-relay" => self.iroh_relay,
722 "iroh" | "iroh-quic" => self.iroh,
723 "ble" => self.ble,
724 "moq" => self.moq,
725 _ => None,
726 }
727 }
728}
729
730#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
731#[serde(rename_all = "camelCase")]
732pub struct DeviceStatusSnapshot {
733 #[serde(flatten)]
734 pub device: crate::signaling::Device,
735 pub presence_status: DevicePresenceStatus,
736 pub presence_updated_at: Option<i64>,
737 pub presence_expires_at: Option<i64>,
738 pub connectable: bool,
739 pub connection_status: DeviceConnectionStatus,
740 pub settled_ready: bool,
741 pub readiness_state: ReadinessState,
742 pub readiness_reason: String,
743 pub peer_health: crate::connection_manager::ConnectionHealth,
744 pub peer_id: Option<String>,
745 pub scopes: Vec<String>,
746 pub connection_id: Option<String>,
747 pub device_id_hint: Option<String>,
748 pub active_transport_stable_id: Option<u64>,
749 pub transport_generation: u64,
750 pub route_generation: u64,
751 pub active_transport: String,
752 pub parallel_transport: Option<String>,
753 #[serde(default)]
754 pub latency_ms: Option<u64>,
755 #[serde(default)]
756 pub latency_by_transport: TransportLatencySnapshot,
757}
758
759#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
760#[serde(rename_all = "camelCase")]
761pub struct PeerSessionSnapshot {
762 pub peer_id: String,
763 pub device_id: Option<String>,
764 pub device_id_hint: Option<String>,
765 pub node_id: Option<String>,
766 pub active_connection_id: Option<String>,
767 pub candidate_connection_ids: Vec<String>,
768 pub status: crate::connection_manager::ConnectionState,
769 pub health: crate::connection_manager::ConnectionHealth,
770 pub settled_ready: bool,
771 pub readiness_state: ReadinessState,
772 pub active_transport_stable_id: Option<u64>,
773 pub transport_generation: u64,
774 pub route_generation: u64,
775 pub active_transport: String,
776 pub parallel_transport: Option<String>,
777 pub replacement_pending: bool,
778 pub last_lifecycle_transition_at_ms: i64,
779 pub readiness_reason: String,
780 pub transition_count: u64,
781 pub connecting_transition_count: u64,
782 pub replacement_count: u64,
783 pub retire_count: u64,
784 pub last_disconnect_reason: Option<String>,
785 pub last_reconnect_reason: Option<String>,
786 pub scopes: Vec<String>,
787 pub last_seen_at_ms: i64,
788 pub error: Option<String>,
789}
790
791#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
792#[serde(rename_all = "camelCase")]
793pub struct ManagedConnectResult {
794 pub connection_id: String,
795 pub device_id: Option<String>,
796 pub device_id_hint: Option<String>,
797 pub remote_node_id: String,
798 pub state: String,
799 #[serde(default, skip_serializing_if = "Option::is_none")]
800 pub approved_scope: Option<String>,
801}
802
803#[cfg(not(target_arch = "wasm32"))]
804#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
805#[serde(rename_all = "camelCase")]
806pub struct ManagedConnectionAdoption {
807 pub connection_id: String,
808 pub node_id: String,
809 pub device_id: Option<String>,
810 pub transport_generation: u64,
811 pub status_reason: Option<String>,
812 pub main_stream_ready: bool,
813}
814
815#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
816#[serde(rename_all = "camelCase")]
817pub enum ManagedConnectionHealthStatus {
818 Healthy,
819 AwaitingReplacement,
820 Dead,
821}
822
823#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
824#[serde(rename_all = "camelCase")]
825pub struct ManagedConnectionHealthSnapshot {
826 pub connection_id: String,
827 pub device_id: Option<String>,
828 pub device_id_hint: Option<String>,
829 pub node_id: Option<String>,
830 pub active_transport_stable_id: Option<u64>,
831 pub transport_generation: u64,
832 pub route_generation: u64,
833 pub status: ManagedConnectionHealthStatus,
834 pub settled_ready: bool,
835 pub readiness_state: ReadinessState,
836 pub replacement_pending: bool,
837 pub last_lifecycle_transition_at_ms: i64,
838 pub readiness_reason: String,
839 pub transition_count: u64,
840 pub connecting_transition_count: u64,
841 pub replacement_count: u64,
842 pub retire_count: u64,
843 pub last_disconnect_reason: Option<String>,
844 pub last_reconnect_reason: Option<String>,
845}
846
847#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
848#[serde(rename_all = "camelCase")]
849pub struct ConnectionStateSnapshot {
850 pub connection_id: String,
851 pub device_id: Option<String>,
852 pub device_id_hint: Option<String>,
853 pub remote_node_id: Option<String>,
854 pub state: String,
855 pub transport_state: String,
856 pub protocol_state: String,
857 pub routable: bool,
858 pub readiness_state: ReadinessState,
859 pub readiness_reason: String,
860 pub transport_generation: u64,
861 pub route_generation: u64,
862 pub active_transport_stable_id: Option<u64>,
863 pub active_transport: String,
864 pub parallel_transport: Option<String>,
865 pub replacement_in_progress: bool,
866 pub last_lifecycle_transition_at_ms: i64,
867 pub transition_count: u64,
868 pub connecting_transition_count: u64,
869 pub replacement_count: u64,
870 pub retire_count: u64,
871 pub last_disconnect_reason: Option<String>,
872 pub last_reconnect_reason: Option<String>,
873 pub error: Option<String>,
874 pub created_at: i64,
875 pub updated_at: i64,
876}
877
878#[cfg(target_arch = "wasm32")]
879#[derive(Debug, Clone, PartialEq, Eq)]
880struct WasmConnectionStateFingerprint {
881 connection_id: String,
882 device_id: Option<String>,
883 device_id_hint: Option<String>,
884 remote_node_id: Option<String>,
885 state: String,
886 transport_state: String,
887 protocol_state: String,
888 routable: bool,
889 transport_generation: u64,
890 route_generation: u64,
891 active_transport_stable_id: Option<u64>,
892 active_transport: String,
893 parallel_transport: Option<String>,
894 replacement_in_progress: bool,
895 transition_count: u64,
896 connecting_transition_count: u64,
897 replacement_count: u64,
898 retire_count: u64,
899 last_disconnect_reason: Option<String>,
900 last_reconnect_reason: Option<String>,
901 error: Option<String>,
902}
903
904#[cfg(target_arch = "wasm32")]
905impl From<&ConnectionStateSnapshot> for WasmConnectionStateFingerprint {
906 fn from(snapshot: &ConnectionStateSnapshot) -> Self {
907 Self {
908 connection_id: snapshot.connection_id.clone(),
909 device_id: snapshot.device_id.clone(),
910 device_id_hint: snapshot.device_id_hint.clone(),
911 remote_node_id: snapshot.remote_node_id.clone(),
912 state: snapshot.state.clone(),
913 transport_state: snapshot.transport_state.clone(),
914 protocol_state: snapshot.protocol_state.clone(),
915 routable: snapshot.routable,
916 transport_generation: snapshot.transport_generation,
917 route_generation: snapshot.route_generation,
918 active_transport_stable_id: snapshot.active_transport_stable_id,
919 active_transport: snapshot.active_transport.clone(),
920 parallel_transport: snapshot.parallel_transport.clone(),
921 replacement_in_progress: snapshot.replacement_in_progress,
922 transition_count: snapshot.transition_count,
923 connecting_transition_count: snapshot.connecting_transition_count,
924 replacement_count: snapshot.replacement_count,
925 retire_count: snapshot.retire_count,
926 last_disconnect_reason: snapshot.last_disconnect_reason.clone(),
927 last_reconnect_reason: snapshot.last_reconnect_reason.clone(),
928 error: snapshot.error.clone(),
929 }
930 }
931}
932
933#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
934#[serde(rename_all = "camelCase", tag = "kind")]
935pub enum ManagedConnectionBridgeAction {
936 Healthy,
937 AwaitReplacement {
938 reason: String,
939 },
940 Rebind {
941 remote_node_id: Option<String>,
942 transport_generation: u64,
943 },
944 Retire {
945 reason: String,
946 },
947}
948
949#[cfg(not(target_arch = "wasm32"))]
950#[derive(Debug, Clone, Copy, PartialEq, Eq)]
951enum DuplicateClosedHandling {
952 RebindKeptTransport,
953 RetireClosedTransport,
954}
955
956#[cfg(not(target_arch = "wasm32"))]
957#[derive(Debug, Clone, Copy, PartialEq, Eq)]
958pub(crate) enum IncomingTransportCloseResolution {
959 PreservedKeptTransport,
960 RetiredClosedTransport,
961}
962
963#[cfg(not(target_arch = "wasm32"))]
964#[derive(Debug, Clone, Copy, PartialEq, Eq)]
965enum AutoConnectTieBreakDecision {
966 ObserveConnectedTransport,
967 WaitForInitiator,
968 ActAsInitiator,
969}
970
971#[cfg(not(target_arch = "wasm32"))]
972fn duplicate_closed_handling(
973 close_reason_debug: &str,
974 kept_transport_alive: bool,
975 kept_transport_healthy: bool,
976 kept_stable_id: u64,
977 closed_stable_id: u64,
978) -> DuplicateClosedHandling {
979 let _ = close_reason_debug;
980 let _ = kept_transport_healthy;
981 if kept_stable_id != closed_stable_id && kept_transport_alive {
982 DuplicateClosedHandling::RebindKeptTransport
983 } else {
984 DuplicateClosedHandling::RetireClosedTransport
985 }
986}
987
988#[cfg(not(target_arch = "wasm32"))]
989fn auto_connect_tie_break_decision(
990 local_node_id: &str,
991 remote_node_id: &str,
992 transport_connected: bool,
993 waited_ms: i64,
994 initiator_grace_ms: i64,
995) -> AutoConnectTieBreakDecision {
996 if transport_connected {
997 return AutoConnectTieBreakDecision::ObserveConnectedTransport;
998 }
999 if local_node_id <= remote_node_id {
1000 if waited_ms >= initiator_grace_ms {
1003 AutoConnectTieBreakDecision::ActAsInitiator
1004 } else {
1005 AutoConnectTieBreakDecision::WaitForInitiator
1006 }
1007 } else {
1008 AutoConnectTieBreakDecision::ActAsInitiator
1009 }
1010}
1011
1012fn normalize_lookup_id(value: Option<&str>) -> Option<String> {
1013 let trimmed = value?.trim();
1014 if trimmed.is_empty() {
1015 None
1016 } else {
1017 Some(trimmed.to_ascii_lowercase())
1018 }
1019}
1020
1021fn peer_snapshot_lookup_aliases(peer: &crate::connection_manager::PeerSnapshot) -> Vec<String> {
1022 [
1023 normalize_lookup_id(peer.device_id.as_deref()),
1024 normalize_lookup_id(peer.device_id_hint.as_deref()),
1025 normalize_lookup_id(peer.node_id.as_deref()),
1026 normalize_lookup_id(Some(peer.peer_id.as_str())),
1027 ]
1028 .into_iter()
1029 .flatten()
1030 .collect()
1031}
1032
1033fn peer_snapshot_matches_any_alias(
1034 peer: &crate::connection_manager::PeerSnapshot,
1035 aliases: &std::collections::HashSet<String>,
1036) -> bool {
1037 peer_snapshot_lookup_aliases(peer)
1038 .into_iter()
1039 .any(|alias| aliases.contains(&alias))
1040}
1041
1042fn preferred_peer_snapshot(
1043 current: Option<crate::connection_manager::PeerSnapshot>,
1044 candidate: crate::connection_manager::PeerSnapshot,
1045) -> crate::connection_manager::PeerSnapshot {
1046 match current {
1047 Some(existing) if !prefer_device_status_peer_snapshot(&candidate, &existing) => existing,
1048 _ => candidate,
1049 }
1050}
1051
1052fn snapshot_connection_status(
1053 device: &crate::signaling::Device,
1054 peer: Option<&crate::connection_manager::PeerSnapshot>,
1055) -> DeviceConnectionStatus {
1056 match peer.map(|value| &value.status) {
1057 Some(crate::connection_manager::ConnectionState::Pending)
1058 | Some(crate::connection_manager::ConnectionState::Connecting) => {
1059 DeviceConnectionStatus::Connecting
1060 }
1061 Some(crate::connection_manager::ConnectionState::Connected)
1062 if peer.map(peer_snapshot_settled_ready).unwrap_or(false) =>
1063 {
1064 DeviceConnectionStatus::Connected
1065 }
1066 Some(crate::connection_manager::ConnectionState::Connected) => {
1067 DeviceConnectionStatus::Connecting
1068 }
1069 Some(crate::connection_manager::ConnectionState::Failed) => DeviceConnectionStatus::Failed,
1070 Some(crate::connection_manager::ConnectionState::Closed)
1071 | Some(crate::connection_manager::ConnectionState::Closing) => {
1072 DeviceConnectionStatus::Closed
1073 }
1074 None if device.online => DeviceConnectionStatus::Online,
1075 None => DeviceConnectionStatus::Disconnected,
1076 }
1077}
1078
1079fn snapshot_settled_ready(
1080 peer: Option<&crate::connection_manager::PeerSnapshot>,
1081 connection_status: &DeviceConnectionStatus,
1082) -> bool {
1083 matches!(connection_status, DeviceConnectionStatus::Connected)
1084 && peer.map(peer_snapshot_settled_ready).unwrap_or(false)
1085}
1086
1087fn device_presence_updated_at(device: &crate::signaling::Device) -> Option<i64> {
1088 [
1089 device.last_seen_at.as_ref(),
1090 device.updated_at.as_ref(),
1091 device.created_at.as_ref(),
1092 ]
1093 .into_iter()
1094 .flatten()
1095 .filter_map(|value| crate::presence_policy::parse_millis(Some(value)))
1096 .max()
1097}
1098
1099fn device_presence_expires_at(device: &crate::signaling::Device) -> Option<i64> {
1100 crate::presence_policy::parse_millis(device.expires_at.as_ref())
1101}
1102
1103fn device_presence_status(
1104 device: &crate::signaling::Device,
1105 presence_updated_at: Option<i64>,
1106 now_ms: i64,
1107) -> DevicePresenceStatus {
1108 if !device.online {
1109 return DevicePresenceStatus::Offline;
1110 }
1111
1112 if presence_updated_at
1113 .map(|updated_at| {
1114 updated_at.saturating_add(crate::presence_policy::DEVICE_STALE_HEARTBEAT_MS) <= now_ms
1115 })
1116 .unwrap_or(false)
1117 {
1118 DevicePresenceStatus::Idle
1119 } else {
1120 DevicePresenceStatus::Online
1121 }
1122}
1123
1124fn device_connectable(
1125 device: &crate::signaling::Device,
1126 presence_status: &DevicePresenceStatus,
1127) -> bool {
1128 matches!(presence_status, DevicePresenceStatus::Online)
1129 && device
1130 .ticket
1131 .as_deref()
1132 .map(str::trim)
1133 .is_some_and(|value| !value.is_empty())
1134}
1135
1136fn peer_snapshot_settled_ready(peer: &crate::connection_manager::PeerSnapshot) -> bool {
1137 matches!(peer_readiness_state(peer), ReadinessState::Routable)
1138}
1139
1140fn peer_has_live_transport(peer: &crate::connection_manager::PeerSnapshot) -> bool {
1141 !peer.connection_ids.is_empty()
1142 && (peer.active_transport_stable_id.is_some()
1143 || crate::transport_label::is_independent_transport(peer.active_transport.as_str()))
1144}
1145
1146fn peer_readiness_state(peer: &crate::connection_manager::PeerSnapshot) -> ReadinessState {
1147 match peer.status {
1148 crate::connection_manager::ConnectionState::Pending
1149 | crate::connection_manager::ConnectionState::Connecting => ReadinessState::Connecting,
1150 crate::connection_manager::ConnectionState::Connected => {
1151 let connection_ids_present = !peer.connection_ids.is_empty();
1155 let healthy_with_connections = connection_ids_present
1156 && matches!(
1157 peer.health,
1158 crate::connection_manager::ConnectionHealth::Healthy
1159 );
1160 if healthy_with_connections {
1161 ReadinessState::Routable
1162 } else if !peer_has_live_transport(peer) {
1163 ReadinessState::TransportOnly
1164 } else if now_millis_i64().saturating_sub(peer.last_lifecycle_transition_at_ms)
1165 >= MANAGED_SETTLE_DEADLINE_MS
1166 {
1167 ReadinessState::AwaitingReplacement
1168 } else {
1169 ReadinessState::Settling
1170 }
1171 }
1172 crate::connection_manager::ConnectionState::Closing
1173 | crate::connection_manager::ConnectionState::Closed => ReadinessState::Closed,
1174 crate::connection_manager::ConnectionState::Failed => ReadinessState::Failed,
1175 }
1176}
1177
1178fn peer_readiness_reason(peer: &crate::connection_manager::PeerSnapshot) -> String {
1179 match peer_readiness_state(peer) {
1180 ReadinessState::Connecting => "waiting-for-transport".to_string(),
1181 ReadinessState::TransportOnly => {
1182 "transport-attached-awaiting-readiness-confirmation".to_string()
1183 }
1184 ReadinessState::Settling => "transport-connected-health-check-pending".to_string(),
1185 ReadinessState::Routable => "transport-healthy".to_string(),
1186 ReadinessState::AwaitingReplacement => "readiness-confirmation-timed-out".to_string(),
1187 ReadinessState::Closed => "transport-closed".to_string(),
1188 ReadinessState::Failed => "connection-failed".to_string(),
1189 }
1190}
1191
1192fn connection_readiness_state(
1193 record: &crate::connection_manager::ConnectionRecord,
1194 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1195) -> ReadinessState {
1196 match record.state {
1197 crate::connection_manager::ConnectionState::Pending
1198 | crate::connection_manager::ConnectionState::Connecting => ReadinessState::Connecting,
1199 crate::connection_manager::ConnectionState::Connected => {
1200 if peer_snapshot
1201 .map(peer_snapshot_settled_ready)
1202 .unwrap_or(false)
1203 {
1204 ReadinessState::Routable
1205 } else if record.transport_stable_id.is_none() {
1206 ReadinessState::AwaitingReplacement
1207 } else if now_millis_i64().saturating_sub(record.last_transport_change_at_ms)
1208 >= MANAGED_SETTLE_DEADLINE_MS
1209 {
1210 ReadinessState::AwaitingReplacement
1211 } else {
1212 ReadinessState::Settling
1213 }
1214 }
1215 crate::connection_manager::ConnectionState::Closing
1216 | crate::connection_manager::ConnectionState::Closed => ReadinessState::Closed,
1217 crate::connection_manager::ConnectionState::Failed => ReadinessState::Failed,
1218 }
1219}
1220
1221fn connection_readiness_reason(
1222 record: &crate::connection_manager::ConnectionRecord,
1223 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1224) -> String {
1225 match connection_readiness_state(record, peer_snapshot) {
1226 ReadinessState::Connecting => "waiting-for-transport".to_string(),
1227 ReadinessState::TransportOnly => {
1228 "transport-attached-awaiting-readiness-confirmation".to_string()
1229 }
1230 ReadinessState::Settling => "bounded-health-confirmation-pending".to_string(),
1231 ReadinessState::Routable => "transport-healthy".to_string(),
1232 ReadinessState::AwaitingReplacement => {
1233 if record.transport_stable_id.is_none() {
1234 "missing-active-transport".to_string()
1235 } else {
1236 "readiness-confirmation-timed-out".to_string()
1237 }
1238 }
1239 ReadinessState::Closed => "transport-closed".to_string(),
1240 ReadinessState::Failed => "connection-failed".to_string(),
1241 }
1242}
1243
1244fn public_peer_state(
1245 peer: &crate::connection_manager::PeerSnapshot,
1246) -> crate::connection_manager::ConnectionState {
1247 if matches!(
1248 peer.status,
1249 crate::connection_manager::ConnectionState::Connected
1250 ) && !peer_snapshot_settled_ready(peer)
1251 {
1252 crate::connection_manager::ConnectionState::Connecting
1253 } else {
1254 peer.status.clone()
1255 }
1256}
1257
1258fn peer_session_snapshot_from_peer(
1259 peer: crate::connection_manager::PeerSnapshot,
1260) -> PeerSessionSnapshot {
1261 let status = public_peer_state(&peer);
1262 let active_connection_id = peer.connection_ids.first().cloned();
1263 let candidate_connection_ids = if peer.connection_ids.len() > 1 {
1264 peer.connection_ids[1..].to_vec()
1265 } else {
1266 Vec::new()
1267 };
1268 let settled_ready = peer_snapshot_settled_ready(&peer);
1269 let readiness_state = peer_readiness_state(&peer);
1270 let readiness_reason = peer_readiness_reason(&peer);
1271 let crate::connection_manager::PeerSnapshot {
1272 peer_id,
1273 device_id,
1274 device_id_hint,
1275 node_id,
1276 active_transport_stable_id,
1277 active_transport_generation,
1278 active_route_generation,
1279 active_transport,
1280 parallel_transport,
1281 last_lifecycle_transition_at_ms,
1282 health,
1283 transition_count,
1284 connecting_transition_count,
1285 replacement_count,
1286 retire_count,
1287 last_disconnect_reason,
1288 last_reconnect_reason,
1289 scopes,
1290 last_seen_at_ms,
1291 error,
1292 ..
1293 } = peer;
1294
1295 PeerSessionSnapshot {
1296 peer_id,
1297 device_id,
1298 device_id_hint,
1299 node_id,
1300 active_connection_id,
1301 candidate_connection_ids,
1302 status,
1303 health,
1304 settled_ready,
1305 readiness_state: readiness_state.clone(),
1306 active_transport_stable_id,
1307 transport_generation: active_transport_generation,
1308 route_generation: active_route_generation,
1309 active_transport,
1310 parallel_transport,
1311 replacement_pending: matches!(readiness_state, ReadinessState::AwaitingReplacement),
1312 last_lifecycle_transition_at_ms,
1313 readiness_reason,
1314 transition_count,
1315 connecting_transition_count,
1316 replacement_count,
1317 retire_count,
1318 last_disconnect_reason,
1319 last_reconnect_reason,
1320 scopes,
1321 last_seen_at_ms,
1322 error,
1323 }
1324}
1325
1326fn connection_state_snapshot_from_parts(
1327 record: &crate::connection_manager::ConnectionRecord,
1328 peer_snapshot: Option<&crate::connection_manager::PeerSnapshot>,
1329) -> ConnectionStateSnapshot {
1330 let settled_ready = peer_snapshot
1331 .map(peer_snapshot_settled_ready)
1332 .unwrap_or(false);
1333 let readiness_state = connection_readiness_state(record, peer_snapshot);
1334 let in_place_reconnect_close = matches!(
1351 record.state,
1352 crate::connection_manager::ConnectionState::Closing
1353 | crate::connection_manager::ConnectionState::Closed
1354 ) && crate::lifecycle_reason::LifecycleReasonCode::from_text(
1355 record.status_reason.as_deref(),
1356 )
1357 .map(|code| {
1358 matches!(
1359 code,
1360 crate::lifecycle_reason::LifecycleReasonCode::ReplacementInProgress
1361 ) || code.is_transient_reconnect()
1362 })
1363 .unwrap_or(false);
1364 let state = match record.state {
1365 crate::connection_manager::ConnectionState::Pending => "connecting",
1366 crate::connection_manager::ConnectionState::Connecting => "connecting",
1367 crate::connection_manager::ConnectionState::Connected if settled_ready => "connected",
1368 crate::connection_manager::ConnectionState::Connected => "connecting",
1369 crate::connection_manager::ConnectionState::Failed => "failed",
1370 crate::connection_manager::ConnectionState::Closing
1371 | crate::connection_manager::ConnectionState::Closed
1372 if in_place_reconnect_close =>
1373 {
1374 "connecting"
1375 }
1376 crate::connection_manager::ConnectionState::Closing
1377 | crate::connection_manager::ConnectionState::Closed => "closed",
1378 };
1379 let transport_state = match record.state {
1380 crate::connection_manager::ConnectionState::Pending
1381 | crate::connection_manager::ConnectionState::Connecting => "connecting",
1382 crate::connection_manager::ConnectionState::Connected => "connected",
1383 crate::connection_manager::ConnectionState::Closing
1384 | crate::connection_manager::ConnectionState::Closed
1385 if in_place_reconnect_close =>
1386 {
1387 "connecting"
1388 }
1389 crate::connection_manager::ConnectionState::Failed
1390 | crate::connection_manager::ConnectionState::Closing
1391 | crate::connection_manager::ConnectionState::Closed => "closed",
1392 };
1393 let protocol_state = match record.state {
1394 crate::connection_manager::ConnectionState::Connected if settled_ready => "routable",
1395 crate::connection_manager::ConnectionState::Connected => "transport-only",
1396 crate::connection_manager::ConnectionState::Pending
1397 | crate::connection_manager::ConnectionState::Connecting => "connecting",
1398 crate::connection_manager::ConnectionState::Closing
1399 | crate::connection_manager::ConnectionState::Closed
1400 if in_place_reconnect_close =>
1401 {
1402 "connecting"
1403 }
1404 crate::connection_manager::ConnectionState::Failed
1405 | crate::connection_manager::ConnectionState::Closing
1406 | crate::connection_manager::ConnectionState::Closed => "closed",
1407 };
1408
1409 ConnectionStateSnapshot {
1410 connection_id: record.connection_id.clone(),
1411 device_id: record
1412 .device_id
1413 .clone()
1414 .or_else(|| peer_snapshot.and_then(|snapshot| snapshot.device_id.clone())),
1415 device_id_hint: record
1416 .device_id_hint
1417 .clone()
1418 .or_else(|| peer_snapshot.and_then(|snapshot| snapshot.device_id_hint.clone())),
1419 remote_node_id: record.node_id.clone(),
1420 state: state.to_string(),
1421 transport_state: transport_state.to_string(),
1422 protocol_state: protocol_state.to_string(),
1423 routable: settled_ready,
1424 readiness_state: readiness_state.clone(),
1425 readiness_reason: connection_readiness_reason(record, peer_snapshot),
1426 transport_generation: record.transport_generation,
1427 route_generation: record.route_generation,
1428 active_transport_stable_id: record.transport_stable_id,
1429 active_transport: record.active_transport.clone(),
1430 parallel_transport: record.parallel_transport.clone(),
1431 replacement_in_progress: matches!(readiness_state, ReadinessState::AwaitingReplacement),
1432 last_lifecycle_transition_at_ms: record
1433 .last_transport_change_at_ms
1434 .max(record.updated_at_ms),
1435 transition_count: record.transition_count,
1436 connecting_transition_count: record.connecting_transition_count,
1437 replacement_count: record.replacement_count,
1438 retire_count: record.retire_count,
1439 last_disconnect_reason: record.last_disconnect_reason.clone(),
1440 last_reconnect_reason: record.last_reconnect_reason.clone(),
1441 error: record.status_reason.clone(),
1442 created_at: record.created_at_ms,
1443 updated_at: record.updated_at_ms,
1444 }
1445}
1446
1447fn merge_device_status_snapshots(
1448 devices: Vec<crate::signaling::Device>,
1449 peers: Vec<crate::connection_manager::PeerSnapshot>,
1450) -> Vec<DeviceStatusSnapshot> {
1451 let mut peer_by_device: std::collections::HashMap<
1452 String,
1453 crate::connection_manager::PeerSnapshot,
1454 > = std::collections::HashMap::new();
1455
1456 for peer in peers {
1457 for alias in peer_snapshot_lookup_aliases(&peer) {
1458 let replace = match peer_by_device.get(&alias) {
1459 Some(existing) => prefer_device_status_peer_snapshot(&peer, existing),
1460 None => true,
1461 };
1462 if replace {
1463 peer_by_device.insert(alias, peer.clone());
1464 }
1465 }
1466 }
1467
1468 devices
1469 .into_iter()
1470 .map(|device| {
1471 let authoritative_node_id = normalize_lookup_id(device.node_id.as_deref());
1472 let lookup_aliases: Vec<String> = [
1473 normalize_lookup_id(Some(device.device_id.as_str())),
1474 authoritative_node_id.clone(),
1475 ]
1476 .into_iter()
1477 .flatten()
1478 .collect();
1479 let peer = lookup_aliases
1480 .iter()
1481 .filter_map(|key| peer_by_device.get(key).cloned())
1482 .filter(|candidate| {
1483 let candidate_node_id = normalize_lookup_id(candidate.node_id.as_deref());
1484 match (&authoritative_node_id, candidate_node_id) {
1485 (Some(expected), Some(actual)) => expected == &actual,
1486 _ => true,
1487 }
1488 })
1489 .fold(None, |current, candidate| {
1490 Some(preferred_peer_snapshot(current, candidate))
1491 });
1492 let connection_status = snapshot_connection_status(&device, peer.as_ref());
1493 let presence_updated_at = device_presence_updated_at(&device);
1494 let presence_expires_at = device_presence_expires_at(&device);
1495 let presence_status =
1496 device_presence_status(&device, presence_updated_at, now_millis_i64());
1497 let connectable = device_connectable(&device, &presence_status);
1498 let readiness_state = peer.as_ref().map(peer_readiness_state).unwrap_or_else(|| {
1499 if device.online {
1500 ReadinessState::Connecting
1501 } else {
1502 ReadinessState::Closed
1503 }
1504 });
1505 DeviceStatusSnapshot {
1506 presence_status,
1507 presence_updated_at,
1508 presence_expires_at,
1509 connectable,
1510 settled_ready: snapshot_settled_ready(peer.as_ref(), &connection_status),
1511 readiness_state: readiness_state.clone(),
1512 readiness_reason: peer.as_ref().map(peer_readiness_reason).unwrap_or_else(|| {
1513 if device.online {
1514 "device-online-awaiting-runtime-session".to_string()
1515 } else {
1516 "device-offline".to_string()
1517 }
1518 }),
1519 connection_status,
1520 peer_health: peer
1521 .as_ref()
1522 .map(|value| value.health.clone())
1523 .unwrap_or_else(|| {
1524 if device.online {
1525 crate::connection_manager::ConnectionHealth::Healthy
1526 } else {
1527 crate::connection_manager::ConnectionHealth::Unknown
1528 }
1529 }),
1530 peer_id: peer.as_ref().map(|value| value.peer_id.clone()),
1531 scopes: peer
1532 .as_ref()
1533 .map(|value| value.scopes.clone())
1534 .unwrap_or_default(),
1535 connection_id: peer
1536 .as_ref()
1537 .and_then(|value| value.connection_ids.first().cloned()),
1538 device_id_hint: peer.as_ref().and_then(|value| value.device_id_hint.clone()),
1539 active_transport_stable_id: peer
1540 .as_ref()
1541 .and_then(|value| value.active_transport_stable_id),
1542 transport_generation: peer
1543 .as_ref()
1544 .map(|value| value.active_transport_generation)
1545 .unwrap_or(0),
1546 route_generation: peer
1547 .as_ref()
1548 .map(|value| value.active_route_generation)
1549 .unwrap_or(0),
1550 active_transport: peer
1551 .as_ref()
1552 .map(|value| value.active_transport.clone())
1553 .unwrap_or_else(|| "iroh".to_string()),
1554 parallel_transport: peer
1555 .as_ref()
1556 .and_then(|value| value.parallel_transport.clone()),
1557 latency_ms: None,
1558 latency_by_transport: TransportLatencySnapshot::default(),
1559 device,
1560 }
1561 })
1562 .collect()
1563}
1564
1565fn peer_snapshot_recency_ms(peer: &crate::connection_manager::PeerSnapshot) -> i64 {
1566 peer.last_seen_at_ms
1567 .max(peer.last_lifecycle_transition_at_ms)
1568}
1569
1570fn prefer_device_status_peer_snapshot(
1576 candidate: &crate::connection_manager::PeerSnapshot,
1577 existing: &crate::connection_manager::PeerSnapshot,
1578) -> bool {
1579 use crate::connection_manager::ConnectionState;
1580 let c = &candidate.status;
1581 let e = &existing.status;
1582 let c_fail = matches!(c, ConnectionState::Failed);
1583 let c_conn = matches!(c, ConnectionState::Connected);
1584 let e_fail = matches!(e, ConnectionState::Failed);
1585 let e_conn = matches!(e, ConnectionState::Connected);
1586
1587 let c_live = c_conn && peer_has_live_transport(candidate);
1593 let e_live = e_conn && peer_has_live_transport(existing);
1594 if c_live && !e_live {
1595 return true;
1596 }
1597 if !c_live && e_live {
1598 return false;
1599 }
1600
1601 if c_fail && e_conn {
1602 return peer_snapshot_recency_ms(candidate) >= peer_snapshot_recency_ms(existing);
1603 }
1604 if c_conn && e_fail {
1605 return peer_snapshot_recency_ms(candidate) > peer_snapshot_recency_ms(existing);
1606 }
1607 device_status_peer_priority(candidate) > device_status_peer_priority(existing)
1608}
1609
1610fn device_status_peer_priority(
1611 peer: &crate::connection_manager::PeerSnapshot,
1612) -> (u8, u8, u8, u8, i64, u64, u64) {
1613 let readiness_rank = match peer_readiness_state(peer) {
1614 ReadinessState::Routable => 5,
1615 ReadinessState::Settling => 4,
1616 ReadinessState::TransportOnly => 3,
1617 ReadinessState::Connecting => 2,
1618 ReadinessState::AwaitingReplacement => 1,
1619 ReadinessState::Closed | ReadinessState::Failed => 0,
1620 };
1621
1622 let status_rank = match peer.status {
1623 crate::connection_manager::ConnectionState::Connected => 3,
1624 crate::connection_manager::ConnectionState::Connecting
1625 | crate::connection_manager::ConnectionState::Pending => 2,
1626 crate::connection_manager::ConnectionState::Closing
1627 | crate::connection_manager::ConnectionState::Closed => 1,
1628 crate::connection_manager::ConnectionState::Failed => 0,
1629 };
1630
1631 let health_rank = match peer.health {
1632 crate::connection_manager::ConnectionHealth::Healthy => 3,
1633 crate::connection_manager::ConnectionHealth::Suspect => 2,
1634 crate::connection_manager::ConnectionHealth::Unknown => 1,
1635 crate::connection_manager::ConnectionHealth::Stale => 0,
1636 };
1637
1638 (
1639 readiness_rank,
1640 status_rank,
1641 health_rank,
1642 u8::from(peer_has_live_transport(peer)),
1643 peer.last_seen_at_ms,
1644 peer.transition_count,
1645 peer.replacement_count,
1646 )
1647}
1648
1649#[cfg(not(target_arch = "wasm32"))]
1650fn auto_connect_verbose() -> bool {
1651 std::env::var("OPENRTC_AUTOCONNECT_VERBOSE")
1652 .map(|value| {
1653 let normalized = value.trim().to_ascii_lowercase();
1654 normalized == "1" || normalized == "true" || normalized == "yes"
1655 })
1656 .unwrap_or(false)
1657}
1658
1659#[cfg(not(target_arch = "wasm32"))]
1660fn parse_env_bool(value: &str) -> Option<bool> {
1661 match value.trim().to_ascii_lowercase().as_str() {
1662 "1" | "true" | "yes" | "on" => Some(true),
1663 "0" | "false" | "no" | "off" => Some(false),
1664 _ => None,
1665 }
1666}
1667
1668#[cfg(not(target_arch = "wasm32"))]
1669fn env_bool(name: &str) -> Option<bool> {
1670 std::env::var(name)
1671 .ok()
1672 .and_then(|value| parse_env_bool(&value))
1673}
1674
1675#[cfg(not(target_arch = "wasm32"))]
1676fn should_use_relay_only_mode() -> bool {
1677 env_bool("PLUTO_IROH_RELAY_ONLY").unwrap_or(false)
1678}
1679
1680#[cfg(not(target_arch = "wasm32"))]
1681fn should_disable_ipv6() -> bool {
1682 resolve_disable_ipv6(
1683 env_bool("PLUTO_IROH_DISABLE_IPV6"),
1684 env_bool("PLUTO_IROH_ENABLE_IPV6"),
1685 )
1686}
1687
1688#[cfg(not(target_arch = "wasm32"))]
1689fn resolve_disable_ipv6(disable: Option<bool>, enable: Option<bool>) -> bool {
1690 disable
1691 .or_else(|| enable.map(|value| !value))
1692 .unwrap_or(false)
1693}
1694
1695#[cfg(not(target_arch = "wasm32"))]
1696fn apply_native_network_preferences(
1697 mut builder: iroh::endpoint::Builder,
1698 relay_only: bool,
1699 relay_transport_policy: IrohRelayTransportPolicy,
1700) -> anyhow::Result<iroh::endpoint::Builder> {
1701 #[cfg(openrtc_iroh_relay_transport_policy_api)]
1702 {
1703 let policy = match relay_transport_policy {
1704 IrohRelayTransportPolicy::Auto => iroh::RelayTransportPolicy::Auto,
1705 IrohRelayTransportPolicy::QuicRequired => iroh::RelayTransportPolicy::QuicRequired,
1706 IrohRelayTransportPolicy::WebsocketRequired => {
1707 iroh::RelayTransportPolicy::WebsocketRequired
1708 }
1709 };
1710 builder = builder.relay_transport_policy(policy);
1711 }
1712 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
1713 {
1714 match relay_transport_policy {
1715 IrohRelayTransportPolicy::QuicRequired => {
1716 anyhow::bail!(
1717 "requested relay carriage quicRequired is unsupported by this upstream-Iroh build; refusing to initialize"
1718 );
1719 }
1720 IrohRelayTransportPolicy::Auto => {
1721 eprintln!(
1722 "[pluto-rtc][native] requested relay carriage auto; effective carriage is websocket fallback because this upstream-Iroh build has no QUIC relay policy API"
1723 );
1724 }
1725 IrohRelayTransportPolicy::WebsocketRequired => {}
1726 }
1727 }
1728
1729 builder = builder.transport_config(crate::native_node::native_iroh_transport_config());
1746
1747 if relay_only {
1748 return Ok(builder.clear_ip_transports());
1750 }
1751
1752 if should_disable_ipv6() {
1753 builder = builder.clear_ip_transports();
1756 builder = builder
1757 .bind_addr("0.0.0.0:0")
1758 .map_err(|e| anyhow::anyhow!("failed to bind IPv4 transport: {}", e))?;
1759 }
1760
1761 Ok(builder)
1762}
1763
1764#[cfg(not(target_arch = "wasm32"))]
1765fn auto_connect_failure_backoff_ms(failure_count: u8) -> i64 {
1766 match failure_count {
1767 0 => 0,
1768 _ => {
1769 let exp = 500_i64.saturating_mul(1_i64 << (failure_count as u32).min(4));
1773 exp.min(8_000)
1774 }
1775 }
1776}
1777
1778#[cfg(not(target_arch = "wasm32"))]
1779fn auto_connect_admission_rejection_backoff_ms(failure_count: u8) -> i64 {
1780 match failure_count {
1781 0 => 0,
1782 _ => {
1783 let exp = 1_000_i64.saturating_mul(1_i64 << (failure_count as u32).min(5));
1787 exp.min(30_000)
1788 }
1789 }
1790}
1791
1792#[cfg(not(target_arch = "wasm32"))]
1793fn auto_connect_network_change_failure_threshold() -> u8 {
1794 std::env::var("OPENRTC_NETWORK_CHANGE_FAILURE_THRESHOLD")
1795 .ok()
1796 .and_then(|value| value.parse::<u8>().ok())
1797 .map(|value| value.clamp(1, 6))
1798 .unwrap_or(2)
1799}
1800
1801#[cfg(not(target_arch = "wasm32"))]
1805fn non_initiator_escalation_grace_ms(escalation_count: u8) -> i64 {
1806 let base_ms = 2_000_i64;
1807 let exp_ms = base_ms.saturating_mul(1_i64 << (escalation_count as u32).min(3));
1808 let capped_ms = exp_ms.min(16_000);
1809 let jitter_pct = ((escalation_count as i64 * 37 + 7) % 41) - 20; let jitter_ms = capped_ms * jitter_pct / 100;
1813 (capped_ms + jitter_ms).max(1_000)
1814}
1815
1816#[cfg(not(target_arch = "wasm32"))]
1817fn next_non_initiator_wakeup_delay_ms(
1818 wait_started_at: &std::collections::HashMap<String, i64>,
1819 escalation_count: &std::collections::HashMap<String, u8>,
1820 now_ms: i64,
1821) -> Option<u64> {
1822 wait_started_at
1823 .iter()
1824 .map(|(device_id, started_at)| {
1825 let grace_ms = non_initiator_escalation_grace_ms(
1826 escalation_count.get(device_id).copied().unwrap_or(0),
1827 );
1828 started_at
1829 .saturating_add(grace_ms)
1830 .saturating_sub(now_ms)
1831 .max(1) as u64
1832 })
1833 .min()
1834}
1835
1836#[cfg(not(target_arch = "wasm32"))]
1837fn register_auto_connect_failure(
1838 remote_device_id: &str,
1839 now_ms: i64,
1840 failure_count: &mut std::collections::HashMap<String, u8>,
1841 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
1842) {
1843 let count = failure_count
1844 .entry(remote_device_id.to_string())
1845 .or_insert(0);
1846 *count = count.saturating_add(1).min(10);
1847 let backoff_ms = auto_connect_failure_backoff_ms(*count);
1848 failure_backoff_until.insert(
1849 remote_device_id.to_string(),
1850 now_ms.saturating_add(backoff_ms),
1851 );
1852}
1853
1854#[cfg(not(target_arch = "wasm32"))]
1855fn register_auto_connect_admission_rejection(
1856 remote_device_id: &str,
1857 now_ms: i64,
1858 failure_count: &mut std::collections::HashMap<String, u8>,
1859 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
1860) {
1861 let count = failure_count
1862 .entry(remote_device_id.to_string())
1863 .or_insert(0);
1864 *count = count.saturating_add(1).min(10);
1865 let backoff_ms = auto_connect_admission_rejection_backoff_ms(*count);
1866 failure_backoff_until.insert(
1867 remote_device_id.to_string(),
1868 now_ms.saturating_add(backoff_ms),
1869 );
1870}
1871
1872#[cfg(not(target_arch = "wasm32"))]
1873fn clear_auto_connect_failure_state(
1874 remote_device_id: &str,
1875 failure_count: &mut std::collections::HashMap<String, u8>,
1876 failure_backoff_until: &mut std::collections::HashMap<String, i64>,
1877) {
1878 failure_count.remove(remote_device_id);
1879 failure_backoff_until.remove(remote_device_id);
1880}
1881
1882#[cfg(target_arch = "wasm32")]
1883fn emit_wasm_connection_state_event(snapshot: &ConnectionStateSnapshot) {
1884 use wasm_bindgen::JsValue;
1885
1886 let Some(window) = web_sys::window() else {
1887 return;
1888 };
1889
1890 let detail = js_sys::Object::new();
1891 let _ = js_sys::Reflect::set(
1892 &detail,
1893 &JsValue::from_str("connectionId"),
1894 &JsValue::from_str(snapshot.connection_id.as_str()),
1895 );
1896 let _ = js_sys::Reflect::set(
1897 &detail,
1898 &JsValue::from_str("state"),
1899 &JsValue::from_str(snapshot.state.as_str()),
1900 );
1901 let _ = js_sys::Reflect::set(
1902 &detail,
1903 &JsValue::from_str("createdAt"),
1904 &JsValue::from_f64(snapshot.created_at as f64),
1905 );
1906 let _ = js_sys::Reflect::set(
1907 &detail,
1908 &JsValue::from_str("updatedAt"),
1909 &JsValue::from_f64(snapshot.updated_at as f64),
1910 );
1911 let _ = js_sys::Reflect::set(
1912 &detail,
1913 &JsValue::from_str("transportState"),
1914 &JsValue::from_str(snapshot.transport_state.as_str()),
1915 );
1916 let _ = js_sys::Reflect::set(
1917 &detail,
1918 &JsValue::from_str("protocolState"),
1919 &JsValue::from_str(snapshot.protocol_state.as_str()),
1920 );
1921 let _ = js_sys::Reflect::set(
1922 &detail,
1923 &JsValue::from_str("routable"),
1924 &JsValue::from_bool(snapshot.routable),
1925 );
1926 let _ = js_sys::Reflect::set(
1927 &detail,
1928 &JsValue::from_str("transportGeneration"),
1929 &JsValue::from_f64(snapshot.transport_generation as f64),
1930 );
1931 let _ = js_sys::Reflect::set(
1932 &detail,
1933 &JsValue::from_str("routeGeneration"),
1934 &JsValue::from_f64(snapshot.route_generation as f64),
1935 );
1936 let _ = js_sys::Reflect::set(
1937 &detail,
1938 &JsValue::from_str("activeTransportStableId"),
1939 &snapshot
1940 .active_transport_stable_id
1941 .map(|value| JsValue::from_f64(value as f64))
1942 .unwrap_or(JsValue::NULL),
1943 );
1944 let _ = js_sys::Reflect::set(
1945 &detail,
1946 &JsValue::from_str("activeTransport"),
1947 &JsValue::from_str(snapshot.active_transport.as_str()),
1948 );
1949 let _ = js_sys::Reflect::set(
1950 &detail,
1951 &JsValue::from_str("parallelTransport"),
1952 &snapshot
1953 .parallel_transport
1954 .as_deref()
1955 .map(JsValue::from_str)
1956 .unwrap_or(JsValue::NULL),
1957 );
1958 let _ = js_sys::Reflect::set(
1959 &detail,
1960 &JsValue::from_str("replacementInProgress"),
1961 &JsValue::from_bool(snapshot.replacement_in_progress),
1962 );
1963 let _ = js_sys::Reflect::set(
1964 &detail,
1965 &JsValue::from_str("transitionCount"),
1966 &JsValue::from_f64(snapshot.transition_count as f64),
1967 );
1968 let _ = js_sys::Reflect::set(
1969 &detail,
1970 &JsValue::from_str("connectingTransitionCount"),
1971 &JsValue::from_f64(snapshot.connecting_transition_count as f64),
1972 );
1973 let _ = js_sys::Reflect::set(
1974 &detail,
1975 &JsValue::from_str("replacementCount"),
1976 &JsValue::from_f64(snapshot.replacement_count as f64),
1977 );
1978 let _ = js_sys::Reflect::set(
1979 &detail,
1980 &JsValue::from_str("retireCount"),
1981 &JsValue::from_f64(snapshot.retire_count as f64),
1982 );
1983
1984 if let Some(device_id) = snapshot.device_id.as_deref() {
1985 let _ = js_sys::Reflect::set(
1986 &detail,
1987 &JsValue::from_str("deviceId"),
1988 &JsValue::from_str(device_id),
1989 );
1990 }
1991 if let Some(device_id_hint) = snapshot.device_id_hint.as_deref() {
1992 let _ = js_sys::Reflect::set(
1993 &detail,
1994 &JsValue::from_str("deviceIdHint"),
1995 &JsValue::from_str(device_id_hint),
1996 );
1997 }
1998 if let Some(remote_node_id) = snapshot.remote_node_id.as_deref() {
1999 let _ = js_sys::Reflect::set(
2000 &detail,
2001 &JsValue::from_str("remoteNodeId"),
2002 &JsValue::from_str(remote_node_id),
2003 );
2004 }
2005 if let Some(error) = snapshot.error.as_deref() {
2006 let _ = js_sys::Reflect::set(
2007 &detail,
2008 &JsValue::from_str("error"),
2009 &JsValue::from_str(error),
2010 );
2011 }
2012 if let Some(reason) = snapshot.last_disconnect_reason.as_deref() {
2013 let _ = js_sys::Reflect::set(
2014 &detail,
2015 &JsValue::from_str("lastDisconnectReason"),
2016 &JsValue::from_str(reason),
2017 );
2018 }
2019 if let Some(reason) = snapshot.last_reconnect_reason.as_deref() {
2020 let _ = js_sys::Reflect::set(
2021 &detail,
2022 &JsValue::from_str("lastReconnectReason"),
2023 &JsValue::from_str(reason),
2024 );
2025 }
2026
2027 let init = web_sys::CustomEventInit::new();
2028 init.set_detail(&detail.into());
2029 if let Ok(event) =
2030 web_sys::CustomEvent::new_with_event_init_dict("connection-state-changed", &init)
2031 {
2032 let _ = window.dispatch_event(&event);
2033 }
2034}
2035
2036fn parse_endpoint_ticket(ticket: &str) -> anyhow::Result<iroh::EndpointAddr> {
2037 let trimmed = ticket.trim();
2038 if trimmed.is_empty() {
2039 return Err(anyhow::anyhow!("endpoint ticket is required"));
2040 }
2041
2042 let parsed = EndpointTicket::from_str(trimmed)
2043 .map_err(|e| anyhow::anyhow!("invalid endpoint ticket: {}", e))?;
2044 Ok(parsed.endpoint_addr().clone())
2045}
2046
2047#[derive(Debug, Clone, serde::Serialize)]
2048#[serde(rename_all = "camelCase")]
2049pub struct RuntimeStatus {
2050 pub ready: bool,
2051 pub node_id: Option<String>,
2052 pub transport: RuntimeTransportStatus,
2053}
2054
2055#[derive(Debug, Clone, serde::Serialize)]
2056#[serde(rename_all = "camelCase")]
2057pub struct RuntimeTransportFeatureStatus {
2058 pub compiled: bool,
2059 pub enabled: bool,
2060}
2061
2062impl RuntimeTransportFeatureStatus {
2063 pub(crate) fn new(compiled: bool, enabled: bool) -> Self {
2064 Self {
2065 compiled,
2066 enabled: compiled && enabled,
2067 }
2068 }
2069}
2070
2071#[derive(Debug, Clone, serde::Serialize)]
2072#[serde(rename_all = "camelCase")]
2073pub struct RuntimeTransportStatus {
2074 pub iroh_quic: RuntimeTransportFeatureStatus,
2075 pub iroh_lan: RuntimeTransportFeatureStatus,
2076 pub web_rtc: RuntimeTransportFeatureStatus,
2077 pub web_rtc_lan: RuntimeTransportFeatureStatus,
2078 pub moq: RuntimeTransportFeatureStatus,
2079 pub ble: RuntimeTransportFeatureStatus,
2080 pub iroh_relay_only: bool,
2081 #[serde(skip_serializing_if = "Option::is_none")]
2082 pub iroh_relay_transport_policy: Option<IrohRelayTransportPolicy>,
2083 pub iroh_relay: IrohRelayDiagnosticStatus,
2084}
2085
2086#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
2087#[serde(rename_all = "camelCase")]
2088pub enum IrohRelayProvider {
2089 VendoredIroh,
2091 UpstreamIroh,
2093}
2094
2095#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
2096#[serde(rename_all = "camelCase")]
2097pub enum IrohRelayEffectiveCarriage {
2098 Quic,
2099 Websocket,
2100 QuicWithWebsocketFallback,
2101 WebsocketFallback,
2102 Unsupported,
2103}
2104
2105#[derive(Debug, Clone, serde::Serialize)]
2106#[serde(rename_all = "camelCase")]
2107pub struct IrohRelayDiagnosticStatus {
2108 pub provider: IrohRelayProvider,
2109 pub requested_carriage: IrohRelayTransportPolicy,
2110 pub effective_carriage: IrohRelayEffectiveCarriage,
2111}
2112
2113impl IrohRelayDiagnosticStatus {
2114 pub(crate) fn from_policy(
2115 requested_carriage: IrohRelayTransportPolicy,
2116 provider: IrohRelayProvider,
2117 ) -> Self {
2118 let effective_carriage = match requested_carriage {
2119 IrohRelayTransportPolicy::WebsocketRequired => IrohRelayEffectiveCarriage::Websocket,
2120 IrohRelayTransportPolicy::Auto => {
2121 #[cfg(target_arch = "wasm32")]
2122 {
2123 IrohRelayEffectiveCarriage::Websocket
2124 }
2125 #[cfg(all(not(target_arch = "wasm32"), openrtc_iroh_relay_transport_policy_api))]
2126 {
2127 IrohRelayEffectiveCarriage::QuicWithWebsocketFallback
2128 }
2129 #[cfg(all(
2130 not(target_arch = "wasm32"),
2131 not(openrtc_iroh_relay_transport_policy_api)
2132 ))]
2133 {
2134 IrohRelayEffectiveCarriage::WebsocketFallback
2135 }
2136 }
2137 IrohRelayTransportPolicy::QuicRequired => {
2138 #[cfg(target_arch = "wasm32")]
2139 {
2140 IrohRelayEffectiveCarriage::Unsupported
2141 }
2142 #[cfg(all(not(target_arch = "wasm32"), openrtc_iroh_relay_transport_policy_api))]
2143 {
2144 IrohRelayEffectiveCarriage::Quic
2145 }
2146 #[cfg(all(
2147 not(target_arch = "wasm32"),
2148 not(openrtc_iroh_relay_transport_policy_api)
2149 ))]
2150 {
2151 IrohRelayEffectiveCarriage::Unsupported
2152 }
2153 }
2154 };
2155
2156 Self {
2157 provider,
2158 requested_carriage,
2159 effective_carriage,
2160 }
2161 }
2162}
2163
2164#[cfg(openrtc_iroh_relay_transport_policy_api)]
2165pub(crate) const fn current_iroh_relay_provider() -> IrohRelayProvider {
2166 IrohRelayProvider::VendoredIroh
2167}
2168
2169#[cfg(not(openrtc_iroh_relay_transport_policy_api))]
2170pub(crate) const fn current_iroh_relay_provider() -> IrohRelayProvider {
2171 IrohRelayProvider::UpstreamIroh
2172}
2173
2174impl RuntimeTransportStatus {
2175 pub(crate) fn from_config_with_ble_available(
2176 config: &TransportConfig,
2177 ble_available: bool,
2178 relay_provider: IrohRelayProvider,
2179 ) -> Self {
2180 let lan_compiled = cfg!(feature = "transport-lan");
2181 let lan_enabled = config
2182 .iroh_lan
2183 .as_ref()
2184 .map(|lan| lan.enabled)
2185 .unwrap_or(false);
2186 let webrtc_compiled = cfg!(feature = "transport-webrtc");
2187 let webrtc_enabled = config.webrtc.is_some();
2188 let webrtc_lan_enabled = config
2189 .webrtc
2190 .as_ref()
2191 .map(|webrtc| webrtc.lan_mode)
2192 .unwrap_or(false);
2193 let moq_compiled = cfg!(feature = "transport-moq");
2194 let moq_enabled = config.moq.is_some();
2195 let ble_compiled = ble_available;
2196 let ble_enabled = config.ble.as_ref().map(|ble| ble.enabled).unwrap_or(false);
2197
2198 Self {
2199 iroh_quic: RuntimeTransportFeatureStatus::new(true, true),
2200 iroh_lan: RuntimeTransportFeatureStatus::new(lan_compiled, lan_enabled),
2201 web_rtc: RuntimeTransportFeatureStatus::new(webrtc_compiled, webrtc_enabled),
2202 web_rtc_lan: RuntimeTransportFeatureStatus::new(
2203 webrtc_compiled,
2204 webrtc_enabled && webrtc_lan_enabled,
2205 ),
2206 moq: RuntimeTransportFeatureStatus::new(moq_compiled, moq_enabled),
2207 ble: RuntimeTransportFeatureStatus::new(ble_compiled, ble_enabled),
2208 iroh_relay_only: config.iroh_relay_only,
2209 iroh_relay_transport_policy: config.iroh_relay_transport_policy.clone(),
2210 iroh_relay: IrohRelayDiagnosticStatus::from_policy(
2211 config
2212 .iroh_relay_transport_policy
2213 .clone()
2214 .unwrap_or(IrohRelayTransportPolicy::WebsocketRequired),
2215 relay_provider,
2216 ),
2217 }
2218 }
2219}
2220
2221#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
2222#[serde(rename_all = "camelCase")]
2223#[cfg(feature = "legacy-v1")]
2224pub enum AuthMode {
2225 External,
2226 Anonymous,
2227 Required,
2228}
2229
2230#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2231#[serde(rename_all = "camelCase")]
2232pub struct IceServerConfig {
2233 #[serde(default)]
2234 pub urls: Vec<String>,
2235 #[serde(default, skip_serializing_if = "Option::is_none")]
2236 pub username: Option<String>,
2237 #[serde(default, skip_serializing_if = "Option::is_none")]
2238 pub credential: Option<String>,
2239}
2240
2241#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)]
2242#[serde(rename_all = "camelCase")]
2243pub struct WebRTCConfig {
2244 #[serde(default)]
2245 pub ice_servers: Vec<IceServerConfig>,
2246 #[serde(default)]
2247 pub privacy_mode: bool,
2248 #[serde(default)]
2249 pub lan_mode: bool,
2250}
2251
2252#[derive(Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2253#[serde(rename_all = "camelCase")]
2254pub struct MoQConfig {
2255 #[serde(default)]
2256 pub relay_url: String,
2257 #[serde(default, skip_serializing)]
2258 pub access_token: Option<String>,
2259}
2260
2261impl std::fmt::Debug for MoQConfig {
2262 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2263 let relay_label = self
2264 .relay_url
2265 .split(['?', '#'])
2266 .next()
2267 .unwrap_or("invalid-moq-relay-url");
2268 formatter
2269 .debug_struct("MoQConfig")
2270 .field("relay_url", &relay_label)
2271 .field(
2272 "access_token",
2273 &self.access_token.as_ref().map(|_| "[redacted]"),
2274 )
2275 .finish()
2276 }
2277}
2278
2279impl Default for MoQConfig {
2280 fn default() -> Self {
2281 Self {
2282 relay_url: String::new(),
2283 access_token: None,
2284 }
2285 }
2286}
2287
2288#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2289#[serde(rename_all = "camelCase")]
2290pub enum IrohRelayTransportPolicy {
2291 Auto,
2292 QuicRequired,
2293 WebsocketRequired,
2294}
2295
2296#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
2297#[serde(rename_all = "camelCase")]
2298pub struct TransportConfig {
2299 #[serde(default)]
2300 pub iroh_relay_only: bool,
2301 #[serde(default, skip_serializing_if = "Option::is_none")]
2302 pub iroh_relay_transport_policy: Option<IrohRelayTransportPolicy>,
2303 #[serde(default, skip_serializing_if = "Option::is_none")]
2304 pub iroh_lan: Option<IrohLanConfig>,
2305 #[serde(default, skip_serializing_if = "Option::is_none")]
2306 pub webrtc: Option<WebRTCConfig>,
2307 #[serde(default, skip_serializing_if = "Option::is_none")]
2308 pub moq: Option<MoQConfig>,
2309 #[serde(default, skip_serializing_if = "Option::is_none")]
2310 pub ble: Option<BleConfig>,
2311}
2312
2313impl Default for TransportConfig {
2314 fn default() -> Self {
2315 Self {
2316 iroh_relay_only: false,
2317 iroh_relay_transport_policy: None,
2318 iroh_lan: Some(IrohLanConfig::default()),
2319 webrtc: None,
2320 moq: None,
2321 ble: None,
2322 }
2323 }
2324}
2325
2326impl TransportConfig {
2327 #[cfg(not(target_arch = "wasm32"))]
2331 pub(crate) fn endpoint_rebind_required(&self, next: &Self) -> bool {
2332 self.iroh_relay_only != next.iroh_relay_only
2333 || self.iroh_relay_transport_policy != next.iroh_relay_transport_policy
2334 || self.iroh_lan != next.iroh_lan
2335 }
2336
2337 pub fn sanitize_for_runtime(self) -> Self {
2338 self.sanitize_for_runtime_with_ble_available(false)
2339 }
2340
2341 pub(crate) fn sanitize_for_runtime_with_ble_available(mut self, ble_available: bool) -> Self {
2342 if self.ble.as_ref().map(|ble| ble.enabled).unwrap_or(false) && !ble_available {
2343 self.ble = None;
2344 }
2345
2346 self
2347 }
2348}
2349
2350pub struct ClientBuilder {
2351 project_id: String,
2352 app_tag: String,
2353 transport_config: TransportConfig,
2354 token_provider: Arc<dyn Fn() -> Option<String> + Send + Sync>,
2355 signaling: Option<Arc<dyn SignalingBackend>>,
2356 room: Option<Arc<dyn RoomBackend>>,
2357}
2358
2359impl ClientBuilder {
2360 pub fn new_provider_neutral(
2361 app_tag: String,
2362 identity_credential_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2363 ) -> Self {
2364 Self {
2365 project_id: String::new(),
2369 app_tag,
2370 transport_config: TransportConfig::default(),
2371 token_provider: Arc::from(identity_credential_provider),
2372 signaling: None,
2373 room: None,
2374 }
2375 }
2376
2377 #[cfg(any(test, feature = "legacy-v1"))]
2380 pub fn new(
2381 project_id: String,
2382 api_key: String,
2383 token_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2384 ) -> Self {
2385 Self::new_with_app_tag(
2386 project_id,
2387 crate::app_tag_from_api_key(&api_key),
2388 token_provider,
2389 )
2390 }
2391
2392 #[cfg(any(test, feature = "legacy-v1"))]
2393 pub fn new_with_app_tag(
2394 project_id: String,
2395 app_tag: String,
2396 token_provider: Box<dyn Fn() -> Option<String> + Send + Sync>,
2397 ) -> Self {
2398 Self {
2399 project_id,
2400 app_tag,
2401 transport_config: TransportConfig::default(),
2402 token_provider: Arc::from(token_provider),
2403 signaling: None,
2404 room: None,
2405 }
2406 }
2407
2408 #[cfg(feature = "legacy-v1")]
2409 pub fn auth_mode(self, auth_mode: AuthMode) -> Self {
2410 let _ = auth_mode;
2411 self
2412 }
2413
2414 #[cfg(all(not(target_arch = "wasm32"), feature = "legacy-v1"))]
2415 pub fn native_auth_state(mut self, auth_state: crate::native_auth::NativeAuthState) -> Self {
2416 self.token_provider = Arc::from(auth_state.token_provider());
2417 self
2418 }
2419
2420 pub fn transport_config(mut self, transport_config: TransportConfig) -> Self {
2421 self.transport_config = transport_config.sanitize_for_runtime();
2422 self
2423 }
2424
2425 #[cfg(feature = "legacy-v1")]
2426 pub fn native_trust_token_provider(
2427 self,
2428 provider: Box<dyn Fn() -> Option<NativeTrustToken> + Send + Sync>,
2429 ) -> Self {
2430 let _ = provider;
2431 self
2432 }
2433
2434 pub fn signaling_backend(mut self, signaling: Arc<dyn SignalingBackend>) -> Self {
2435 self.signaling = Some(signaling);
2436 self
2437 }
2438
2439 pub fn room_backend(mut self, room: Arc<dyn RoomBackend>) -> Self {
2440 self.room = Some(room);
2441 self
2442 }
2443
2444 pub fn build(self) -> Client {
2445 let app_backgrounded = Arc::new(AtomicBool::new(false));
2446
2447 let room = self
2448 .room
2449 .unwrap_or_else(|| Arc::new(GatewayRequiredRoomBackend) as Arc<dyn RoomBackend>);
2450
2451 let signaling = self.signaling.unwrap_or_else(|| {
2452 Arc::new(GatewayRequiredSignalingBackend) as Arc<dyn SignalingBackend>
2453 });
2454
2455 #[cfg(not(target_arch = "wasm32"))]
2456 let (native_device_updates, _) = tokio::sync::broadcast::channel(32);
2457 #[cfg(not(target_arch = "wasm32"))]
2458 let (native_connection_state_updates, _) = tokio::sync::broadcast::channel(64);
2459 #[cfg(not(target_arch = "wasm32"))]
2460 let (native_peer_data_updates, _) = tokio::sync::broadcast::channel(256);
2461 #[cfg(not(target_arch = "wasm32"))]
2462 let (native_application_streams, native_application_streams_receiver) =
2463 async_channel::bounded(256);
2464
2465 Client {
2466 app_tag: self.app_tag,
2467 signaling,
2468 room,
2469 node_id: Arc::new(RwLock::new(None)),
2470 iroh_endpoint: Arc::new(RwLock::new(None)),
2471 #[cfg(target_arch = "wasm32")]
2472 iroh_node: Arc::new(RwLock::new(None)),
2473 #[cfg(not(target_arch = "wasm32"))]
2474 iroh_node: Arc::new(RwLock::new(None)),
2475 #[cfg(not(target_arch = "wasm32"))]
2476 native_application_streams,
2477 #[cfg(not(target_arch = "wasm32"))]
2478 native_application_streams_receiver,
2479 connection_manager: Arc::new(crate::connection_manager::ConnectionManager::new()),
2480 session_token_registry: Arc::new(crate::session_token::SessionTokenRegistry::new()),
2481 #[cfg(not(target_arch = "wasm32"))]
2482 inbound_session_admission_transport_ids: Arc::new(StdRwLock::new(HashMap::new())),
2483 #[cfg(not(target_arch = "wasm32"))]
2484 native_admission_stream_contracts: Arc::new(StdRwLock::new(HashMap::new())),
2485 remote_session_admission_proofs: Arc::new(StdRwLock::new(HashMap::new())),
2486 pending_inline_reciprocal_admissions: Arc::new(StdRwLock::new(HashMap::new())),
2487 outbound_application_security_epoch_fingerprints: Arc::new(StdRwLock::new(
2488 HashMap::new(),
2489 )),
2490 #[cfg(not(target_arch = "wasm32"))]
2491 pending_reciprocal_session_admission_requests: Arc::new(StdRwLock::new(HashSet::new())),
2492 #[cfg(not(target_arch = "wasm32"))]
2493 native_route_repair_credentials: Arc::new(StdRwLock::new(HashMap::new())),
2494 connection_application_crypto_keys:
2495 crate::client::application_crypto_impl::new_connection_application_crypto_key_map(),
2496 connection_application_crypto_required:
2497 crate::client::application_crypto_impl::new_connection_application_crypto_required_set(),
2498 #[cfg(not(target_arch = "wasm32"))]
2499 trusted_user_device_application_crypto_required: Arc::new(AtomicBool::new(false)),
2500 connection_application_crypto_confirmed:
2501 crate::client::application_crypto_impl::new_connection_application_crypto_confirmed_set(),
2502 #[cfg(not(target_arch = "wasm32"))]
2503 connection_application_crypto_confirmation_updates: Arc::new(
2504 tokio::sync::Notify::new(),
2505 ),
2506 connection_application_crypto_outbound_sequences:
2507 crate::client::application_crypto_impl::new_connection_application_crypto_outbound_sequences(),
2508 connection_application_key_agreements:
2509 crate::client::application_crypto_impl::new_connection_application_key_agreement_map(),
2510 #[cfg(not(target_arch = "wasm32"))]
2511 managed_scope_tickets: Arc::new(StdRwLock::new(HashMap::new())),
2512 known_endpoint_addrs: Arc::new(RwLock::new(HashMap::new())),
2513 known_device_ids_by_node: Arc::new(StdRwLock::new(HashMap::new())),
2514 #[cfg(not(target_arch = "wasm32"))]
2515 native_device_identity: Arc::new(RwLock::new(None)),
2516 #[cfg(not(target_arch = "wasm32"))]
2517 native_device_base_dir: Arc::new(RwLock::new(None)),
2518 #[cfg(not(target_arch = "wasm32"))]
2519 native_device_identity_init_guard: Arc::new(tokio::sync::Mutex::new(())),
2520 #[cfg(not(target_arch = "wasm32"))]
2521 native_device_updates,
2522 #[cfg(not(target_arch = "wasm32"))]
2523 native_connection_state_updates,
2524 #[cfg(not(target_arch = "wasm32"))]
2525 native_peer_data_updates,
2526 auto_connect_loop_key: Arc::new(Mutex::new(None)),
2527 auto_connect_generation: Arc::new(AtomicU64::new(0)),
2528 #[cfg(not(target_arch = "wasm32"))]
2529 external_desired_peer_actor: Arc::new(tokio::sync::Mutex::new(
2530 auto_connect_impl::NativeExternalAutoConnectActorState::default(),
2531 )),
2532 auto_connect_excluded: Arc::new(Mutex::new(HashSet::new())),
2533 auto_connect_peer_requested_excluded: Arc::new(Mutex::new(HashSet::new())),
2534 auto_connect_excluded_node_aliases: Arc::new(Mutex::new(HashMap::new())),
2535 app_backgrounded,
2536 #[cfg(target_arch = "wasm32")]
2537 wasm_accept_bridge_started: Arc::new(std::sync::atomic::AtomicBool::new(false)),
2538 #[cfg(target_arch = "wasm32")]
2539 last_emitted_connection_states: Arc::new(Mutex::new(std::collections::HashMap::new())),
2540 #[cfg(target_arch = "wasm32")]
2541 last_empty_peer_sessions_warning_ms: Arc::new(AtomicU64::new(0)),
2542 iroh_init_guard: Arc::new(tokio::sync::Mutex::new(())),
2543 managed_connect_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2544 #[cfg(not(target_arch = "wasm32"))]
2545 iroh_path_watcher_stable_ids: Arc::new(Mutex::new(HashSet::new())),
2546 #[cfg(not(target_arch = "wasm32"))]
2547 native_custom_transport_kinds: Arc::new(RwLock::new(HashMap::new())),
2548 #[cfg(not(target_arch = "wasm32"))]
2549 native_transport_upgrade_providers: Arc::new(RwLock::new(HashMap::new())),
2550 #[cfg(not(target_arch = "wasm32"))]
2551 native_transport_upgrade_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2552 #[cfg(not(target_arch = "wasm32"))]
2553 native_ble_upgrade_attempts: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2554 #[cfg(not(target_arch = "wasm32"))]
2555 native_peer_transport_capabilities: Arc::new(RwLock::new(HashMap::new())),
2556 #[cfg(not(target_arch = "wasm32"))]
2557 native_scoped_webrtc_signal_peer_connections: Arc::new(RwLock::new(HashSet::new())),
2558 presence_loop_tx: Arc::new(Mutex::new(None)),
2559 #[cfg(not(target_arch = "wasm32"))]
2560 #[cfg(not(target_arch = "wasm32"))]
2561 project_id: self.project_id,
2562 #[cfg(not(target_arch = "wasm32"))]
2563 token_provider: self.token_provider,
2564 transport_config: Arc::new(RwLock::new(self.transport_config.sanitize_for_runtime())),
2565 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
2566 local_discovery_registry: crate::local_discovery::LocalDiscoveryRegistry::new(),
2567 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
2568 mdns_address_lookup: Arc::new(RwLock::new(None)),
2569 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2570 native_webrtc_sessions: Arc::new(RwLock::new(HashMap::new())),
2571 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2572 native_webrtc_route_proofs: Arc::new(RwLock::new(HashMap::new())),
2573 #[cfg(not(target_arch = "wasm32"))]
2574 native_optional_route_generations: Arc::new(RwLock::new(HashMap::new())),
2575 #[cfg(not(target_arch = "wasm32"))]
2576 native_route_start_gates: Arc::new(
2577 route_adapter::NativeRouteStartGateRegistry::default(),
2578 ),
2579 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2580 native_webrtc_suppressions: Arc::new(RwLock::new(HashMap::new())),
2581 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2582 native_webrtc_attempt_counts: Arc::new(RwLock::new(HashMap::new())),
2583 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2584 native_webrtc_start_gates: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2585 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2586 native_webrtc_attempts_in_flight: Arc::new(RwLock::new(HashMap::new())),
2587 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2588 native_webrtc_retry_deadlines: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2589 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
2590 deferred_managed_retirements: Arc::new(RwLock::new(HashMap::new())),
2591 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
2592 native_moq_sessions: Arc::new(RwLock::new(HashMap::new())),
2593 #[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
2594 native_moq_route_proofs: Arc::new(RwLock::new(HashMap::new())),
2595 #[cfg(not(target_arch = "wasm32"))]
2596 native_control_streams: Arc::new(tokio::sync::Mutex::new(HashMap::new())),
2597 #[cfg(not(target_arch = "wasm32"))]
2598 native_transport_protocol_activity: Arc::new(StdRwLock::new(HashMap::new())),
2599 #[cfg(not(target_arch = "wasm32"))]
2600 pending_native_control_handoffs: Arc::new(StdRwLock::new(HashMap::new())),
2601 #[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
2602 scoped_connection_actor_registry: Arc::new(RwLock::new(None)),
2603 #[cfg(not(target_arch = "wasm32"))]
2604 auth_readiness: Arc::new(crate::client::auth_readiness::AuthReadinessStore::new()),
2605 #[cfg(not(target_arch = "wasm32"))]
2606 scope_classifier: Arc::new(RwLock::new(default_scope_classifier())),
2607 }
2608 }
2609}
2610
2611mod admission_impl;
2619mod application_crypto_impl;
2620#[cfg(not(target_arch = "wasm32"))]
2621pub mod auth_readiness;
2622mod auto_connect_impl;
2623mod core_impl;
2624#[cfg(not(target_arch = "wasm32"))]
2625pub mod correlation;
2626#[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
2627mod drive_grant_actor;
2628#[cfg(all(
2629 test,
2630 not(target_arch = "wasm32"),
2631 feature = "experimental-scoped-actor"
2632))]
2633mod drive_grant_actor_tests;
2634#[cfg(not(target_arch = "wasm32"))]
2635mod route_adapter;
2636#[cfg(not(target_arch = "wasm32"))]
2637pub mod scope_classifier;
2638#[cfg(all(not(target_arch = "wasm32"), feature = "experimental-scoped-actor"))]
2639pub mod scoped_connection_actor;
2640mod state_signaling_impl;
2641#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
2642pub(crate) use route_adapter::NativeOptionalRouteTerminalDisposition;
2643#[cfg(not(target_arch = "wasm32"))]
2644pub(crate) use route_adapter::{NativeOptionalRouteGeneration, NativeOptionalRouteGenerations};
2645#[cfg(not(target_arch = "wasm32"))]
2646mod transport_upgrade_impl;
2647#[cfg(all(not(target_arch = "wasm32"), test))]
2648pub(crate) use transport_upgrade_impl::{
2649 retire_losing_native_control_candidate, retire_replaced_native_control_send,
2650};
2651
2652#[cfg(target_arch = "wasm32")]
2657impl Client {
2658 pub async fn send_peer(&self, _id: &str, _data: &[u8]) -> anyhow::Result<()> {
2659 Err(anyhow::anyhow!(
2660 "send_peer: use TypeScript Connection.sendTyped() for transport-aware sending in browser environments"
2661 ))
2662 }
2663}
2664
2665#[cfg(not(target_arch = "wasm32"))]
2666impl Client {
2667 pub fn subscribe_native_peer_data(
2668 &self,
2669 ) -> tokio::sync::broadcast::Receiver<NativePeerDataEvent> {
2670 self.native_peer_data_updates.subscribe()
2671 }
2672
2673 #[cfg_attr(
2674 not(any(feature = "transport-webrtc", feature = "transport-moq")),
2675 allow(dead_code)
2676 )]
2677 pub(crate) fn emit_native_peer_data(&self, event: NativePeerDataEvent) {
2678 let _ = self.native_peer_data_updates.send(event);
2679 }
2680}
2681
2682#[cfg(test)]
2683mod tests;
2684
2685#[cfg(all(test, not(target_arch = "wasm32")))]
2686mod relay_diagnostic_truth_tests {
2687 use super::*;
2688
2689 #[test]
2690 fn auto_reports_requested_and_effective_relay_carriage() {
2691 let diagnostic = IrohRelayDiagnosticStatus::from_policy(
2692 IrohRelayTransportPolicy::Auto,
2693 current_iroh_relay_provider(),
2694 );
2695
2696 assert_eq!(
2697 diagnostic.requested_carriage,
2698 IrohRelayTransportPolicy::Auto
2699 );
2700 let serialized = serde_json::to_value(&diagnostic).expect("relay diagnostic serializes");
2701 assert_eq!(serialized["requestedCarriage"], "auto");
2702 #[cfg(openrtc_iroh_relay_transport_policy_api)]
2703 assert_eq!(
2704 diagnostic.effective_carriage,
2705 IrohRelayEffectiveCarriage::QuicWithWebsocketFallback
2706 );
2707 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
2708 assert_eq!(
2709 diagnostic.effective_carriage,
2710 IrohRelayEffectiveCarriage::WebsocketFallback
2711 );
2712 }
2713
2714 #[cfg(not(openrtc_iroh_relay_transport_policy_api))]
2715 #[test]
2716 fn upstream_quic_required_fails_closed_before_endpoint_bind() {
2717 let result = apply_native_network_preferences(
2718 iroh::Endpoint::builder(iroh::endpoint::presets::N0),
2719 false,
2720 IrohRelayTransportPolicy::QuicRequired,
2721 );
2722
2723 assert!(result.is_err());
2724 assert!(result
2725 .err()
2726 .expect("unsupported QUIC policy should return an error")
2727 .to_string()
2728 .contains("unsupported by this upstream-Iroh build"));
2729 }
2730
2731 #[cfg(openrtc_iroh_relay_transport_policy_api)]
2732 #[test]
2733 fn vendored_quic_required_reports_quic_and_keeps_endpoint_policy_available() {
2734 let diagnostic = IrohRelayDiagnosticStatus::from_policy(
2735 IrohRelayTransportPolicy::QuicRequired,
2736 current_iroh_relay_provider(),
2737 );
2738
2739 assert_eq!(
2740 diagnostic.effective_carriage,
2741 IrohRelayEffectiveCarriage::Quic
2742 );
2743 assert_eq!(diagnostic.provider, IrohRelayProvider::VendoredIroh);
2744 }
2745}