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