1#![allow(deprecated)]
2
3pub mod application_crypto;
4pub mod application_crypto_streams;
5pub mod broadcast;
6pub mod client;
7pub mod coordination;
8pub mod datagrams;
9pub mod explicit_transfer_crypto;
10pub(crate) mod generated;
11pub mod heartbeat;
12#[cfg(feature = "iroh-carrier-core")]
13pub mod iroh_carrier;
14#[cfg(feature = "iroh-carrier-core")]
15pub mod iroh_carrier_bootstrap;
16pub mod iroh_carrier_kind;
19#[cfg(feature = "iroh-carrier-core")]
20pub mod iroh_carrier_proof;
21pub(crate) mod iroh_connection_policy;
22pub mod key_agreement;
23pub mod lifecycle_reason;
24#[cfg(feature = "managed-group-encryption")]
25#[allow(
26 dead_code,
27 reason = "gated managed-room controller; consumed only by managed gateway adapters"
28)]
29pub(crate) mod managed_group_controller;
30#[cfg(feature = "managed-group-encryption")]
31#[allow(
32 dead_code,
33 reason = "gated managed-room crypto owner; enabled only after the tracked persistence and router milestones"
34)]
35pub(crate) mod managed_group_crypto;
36pub mod media;
37pub mod native_protocol;
38pub mod offline;
39#[cfg(feature = "iroh-carrier-core")]
40pub mod packet_carrier_transport;
41pub mod presence;
42pub(crate) mod presence_policy;
43pub mod protocol_config;
44pub mod route_policy;
45pub mod runtime_policy;
46pub mod session_token;
47pub mod signaling;
48pub mod sparse_fanout;
49pub mod stream_metadata;
50pub(crate) mod transport_generation;
51pub(crate) mod transport_label;
52
53#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
54pub mod local_discovery;
55
56#[cfg(test)]
60pub mod test_constants {
61 pub const TEST_PROJECT_ID: &str = "test-project";
62 pub const TEST_API_KEY: &str = "pk_test_0000000000000000000000000000000000000000";
63}
64
65pub const LIVE_PROJECT_ID: &str = "pluto-rtc-prod";
67
68pub fn validate_api_key(api_key: &str) -> anyhow::Result<&str> {
72 let trimmed = api_key.trim();
73 let valid_prefix = trimmed.starts_with("pk_live_") || trimmed.starts_with("pk_test_");
74 let suffix = trimmed.get(8..).unwrap_or_default();
75 if !valid_prefix || suffix.len() != 40 || !suffix.bytes().all(|byte| byte.is_ascii_hexdigit()) {
76 anyhow::bail!("OpenRTC 2.0 requires a public pk_live_ or pk_test_ API key");
77 }
78 Ok(trimmed)
79}
80
81pub fn app_tag_from_api_key(api_key: &str) -> String {
82 let trimmed = api_key.trim();
83 if trimmed.is_empty() {
84 return "app_anonymous".to_string();
85 }
86
87 let suffix_len = trimmed.len().min(16);
88 format!("app_{}", &trimmed[trimmed.len() - suffix_len..])
89}
90
91pub fn space_app_tag(api_key: &str, space_key: &str) -> String {
92 let input = format!("{}:{}", api_key.trim(), space_key.trim());
93 let digest = <sha2::Sha256 as sha2::Digest>::digest(input.as_bytes());
94 format!("space::{}", hex::encode(digest))
95}
96
97#[cfg(test)]
98mod constructor_contract_tests {
99 use super::*;
100
101 #[test]
102 fn rust_constructor_is_provider_neutral_and_side_effect_free() {
103 let api_key = test_constants::TEST_API_KEY;
104 let client = client::Client::new(api_key.to_string()).expect("valid public API key");
105
106 assert_eq!(client.app_tag(), app_tag_from_api_key(api_key));
107 assert!(client::Client::new("firebase-project-id".to_string()).is_err());
108 }
109}
110
111#[cfg(not(target_arch = "wasm32"))]
112pub fn ensure_rustls() {
113 if rustls::crypto::CryptoProvider::get_default().is_none() {
114 let _ = rustls::crypto::ring::default_provider().install_default();
115 }
116}
117
118#[cfg(not(target_arch = "wasm32"))]
119pub mod adapters;
120
121#[cfg(not(target_arch = "wasm32"))]
122pub(crate) mod native_coordination_gateway;
123
124#[cfg(not(target_arch = "wasm32"))]
125pub mod native;
126
127pub use client::Client;
128#[cfg(not(target_arch = "wasm32"))]
129pub use native::ControlPlane;
130
131pub mod connection_manager;
132
133pub mod protocol_registry;
134
135#[cfg(not(target_arch = "wasm32"))]
136pub mod runtime_manager;
137
138#[cfg(not(target_arch = "wasm32"))]
139pub mod transport;
140
141#[cfg(not(target_arch = "wasm32"))]
142pub use client::EndpointHandle;
143
144#[cfg(all(
145 not(target_arch = "wasm32"),
146 not(any(target_os = "ios", target_os = "android"))
147))]
148pub mod sso;
149
150#[cfg(not(target_arch = "wasm32"))]
151pub mod native_node;
152
153#[cfg(not(target_arch = "wasm32"))]
154pub mod native_device;
155
156#[cfg(all(
157 test,
158 not(target_arch = "wasm32"),
159 feature = "iroh-protocols-wasm",
160 any(feature = "transport-webrtc", feature = "transport-moq")
161))]
162mod native_carrier_protocol_test;
163#[cfg(all(not(target_arch = "wasm32"), feature = "transport-moq"))]
164pub mod native_moq_carrier;
165#[cfg(all(not(target_arch = "wasm32"), feature = "transport-webrtc"))]
166pub mod native_webrtc_carrier;
167
168#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
169mod wasm_docs_persistence;
170#[cfg(all(target_arch = "wasm32", feature = "iroh-protocols-wasm"))]
171mod wasm_indexeddb_blob_store;
172#[cfg(all(target_arch = "wasm32", feature = "transport-moq"))]
173pub mod wasm_moq_carrier;
174#[cfg(target_arch = "wasm32")]
175pub mod wasm_node;
176#[cfg(all(target_arch = "wasm32", feature = "transport-webrtc"))]
177pub mod wasm_webrtc_carrier;
178
179#[cfg(target_arch = "wasm32")]
180#[macro_export]
181macro_rules! console_log {
182 ($($t:tt)*) => (web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format_args!($($t)*).to_string())))
183}
184
185#[cfg(target_arch = "wasm32")]
187pub mod wasm_api {
188 use crate::client::Client;
189 use crate::session_token::split_ticket;
190 use crate::wasm_node::{peer_uni_stream_from_send, BiStream, PeerUniStream};
191 use iroh_tickets::endpoint::EndpointTicket;
192 use std::cell::RefCell;
193 #[cfg(any(
194 feature = "iroh-protocols-wasm",
195 feature = "transport-webrtc",
196 feature = "transport-moq",
197 feature = "managed-group-encryption"
198 ))]
199 use std::collections::HashMap;
200 #[cfg(any(
201 feature = "iroh-protocols-wasm",
202 feature = "transport-webrtc",
203 feature = "transport-moq",
204 feature = "managed-group-encryption"
205 ))]
206 use std::rc::Rc;
207 use std::str::FromStr;
208 use std::sync::{Arc, Mutex};
209 use wasm_bindgen::prelude::*;
210 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
211 use wasm_bindgen_futures::spawn_local;
212 use wasm_streams::readable::sys::ReadableStream as JsReadableStream;
213
214 async fn send_native_signal_control_frame(
215 inner: &Client,
216 endpoint_id: String,
217 frame: Vec<u8>,
218 ) -> Result<(), JsValue> {
219 let endpoint_id: iroh::EndpointId = endpoint_id
220 .parse()
221 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
222 inner
223 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
224 .await
225 .map_err(|error| JsValue::from_str(&error.to_string()))?;
226 let (mut send, _recv) = inner
227 .open_bi_internal(endpoint_id)
228 .await
229 .map_err(|error| JsValue::from_str(&error.to_string()))?;
230 send.write_all(&[0x00])
231 .await
232 .map_err(|error| JsValue::from_str(&error.to_string()))?;
233 send.write_all(&(b"signal".len() as u32).to_be_bytes())
234 .await
235 .map_err(|error| JsValue::from_str(&error.to_string()))?;
236 send.write_all(b"signal")
237 .await
238 .map_err(|error| JsValue::from_str(&error.to_string()))?;
239 send.write_all(&frame)
240 .await
241 .map_err(|error| JsValue::from_str(&error.to_string()))?;
242 crate::application_crypto_streams::PeerSendStream::plain(send)
243 .finish_and_wait_for_peer(std::time::Duration::from_secs(2))
244 .await
245 .map_err(|error| JsValue::from_str(&error.to_string()))
246 }
247
248 #[cfg(feature = "transport-webrtc")]
249 #[derive(Debug, Clone)]
250 struct WasmWebRtcCarrierAttempt {
251 connection_id: String,
252 remote_endpoint_id: iroh::EndpointId,
253 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
254 generation: crate::client::WasmPeerDataGeneration,
255 role: &'static str,
256 prepared: bool,
257 retry_sent: bool,
258 offer_started: bool,
259 remote_ready: bool,
260 completion_started: bool,
261 retry_count: u8,
262 inbound_authorization_expires_at_ms: Option<f64>,
263 }
264
265 #[cfg(feature = "transport-moq")]
266 #[derive(Debug, Clone)]
267 struct WasmMoqCarrierAttempt {
268 connection_id: String,
269 remote_endpoint_id: iroh::EndpointId,
270 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
271 generation: crate::client::WasmPeerDataGeneration,
272 role: &'static str,
273 prepared: bool,
274 retry_sent: bool,
275 retry_count: u8,
276 inbound_authorization_expires_at_ms: Option<f64>,
277 }
278
279 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
280 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
281 struct WasmRemoteCarrierCapabilities {
282 webrtc: bool,
283 moq: bool,
284 }
285
286 #[wasm_bindgen]
287 pub struct WasmPeerDatagramPolicy {
288 inner: RefCell<crate::datagrams::PeerDatagramPolicy>,
289 }
290
291 #[wasm_bindgen]
292 impl WasmPeerDatagramPolicy {
293 #[wasm_bindgen(js_name = setIncomingMaxAge)]
294 pub fn set_incoming_max_age(&self, value: Option<u32>, now_ms: f64) {
295 self.inner
296 .borrow_mut()
297 .set_incoming_max_age_ms(value.map(u64::from), now_ms.max(0.0) as u64);
298 }
299
300 #[wasm_bindgen(js_name = setOutgoingMaxAge)]
301 pub fn set_outgoing_max_age(&self, value: Option<u32>) {
302 self.inner
303 .borrow_mut()
304 .set_outgoing_max_age_ms(value.map(u64::from));
305 }
306
307 #[wasm_bindgen(js_name = setIncomingMaxBufferedDatagrams)]
308 pub fn set_incoming_max_buffered_datagrams(&self, value: u32) -> Result<(), JsValue> {
309 self.inner
310 .borrow_mut()
311 .set_incoming_max_buffered(value as usize)
312 .map_err(|error| JsValue::from_str(&error.to_string()))
313 }
314
315 #[wasm_bindgen(js_name = setOutgoingMaxBufferedDatagrams)]
316 pub fn set_outgoing_max_buffered_datagrams(&self, value: u32) -> Result<(), JsValue> {
317 self.inner
318 .borrow_mut()
319 .set_outgoing_max_buffered(value as usize)
320 .map_err(|error| JsValue::from_str(&error.to_string()))
321 }
322
323 #[wasm_bindgen(js_name = pushIncoming)]
324 pub fn push_incoming(&self, payload: Vec<u8>, now_ms: f64) -> Result<(), JsValue> {
325 self.inner
326 .borrow_mut()
327 .push_incoming(payload, now_ms.max(0.0) as u64)
328 .map_err(|error| JsValue::from_str(&error.to_string()))
329 }
330
331 #[wasm_bindgen(js_name = popIncoming)]
332 pub fn pop_incoming(&self, now_ms: f64) -> Option<Vec<u8>> {
333 self.inner.borrow_mut().pop_incoming(now_ms.max(0.0) as u64)
334 }
335
336 #[wasm_bindgen(js_name = pushOutgoing)]
337 pub fn push_outgoing(&self, payload: Vec<u8>, now_ms: f64) -> Result<bool, JsValue> {
338 self.inner
339 .borrow_mut()
340 .push_outgoing(payload, now_ms.max(0.0) as u64)
341 .map_err(|error| JsValue::from_str(&error.to_string()))
342 }
343
344 #[wasm_bindgen(js_name = popOutgoing)]
345 pub fn pop_outgoing(&self, now_ms: f64) -> Result<JsValue, JsValue> {
346 let Some(outgoing) = self.inner.borrow_mut().pop_outgoing(now_ms.max(0.0) as u64)
347 else {
348 return Ok(JsValue::NULL);
349 };
350 let result = js_sys::Object::new();
351 js_sys::Reflect::set(
352 &result,
353 &JsValue::from_str("payload"),
354 &js_sys::Uint8Array::from(outgoing.payload.as_slice()),
355 )?;
356 js_sys::Reflect::set(
357 &result,
358 &JsValue::from_str("remainingMaxAgeMs"),
359 &outgoing
360 .remaining_max_age_ms
361 .map(|value| JsValue::from_f64(value as f64))
362 .unwrap_or(JsValue::NULL),
363 )?;
364 Ok(result.into())
365 }
366
367 #[wasm_bindgen(js_name = recordSent)]
368 pub fn record_sent(&self, bytes: usize) {
369 self.inner.borrow_mut().record_sent(bytes);
370 }
371
372 #[wasm_bindgen(js_name = recordExpiredOutgoing)]
373 pub fn record_expired_outgoing(&self) {
374 self.inner.borrow_mut().record_expired_outgoing();
375 }
376
377 #[wasm_bindgen(js_name = recordSendFailure)]
378 pub fn record_send_failure(&self) {
379 self.inner.borrow_mut().record_send_failure();
380 }
381
382 #[wasm_bindgen(js_name = getStats)]
383 pub fn get_stats(&self, now_ms: f64) -> Result<JsValue, JsValue> {
384 let stats = self.inner.borrow_mut().stats(now_ms.max(0.0) as u64);
385 serde::Serialize::serialize(&stats, &serde_wasm_bindgen::Serializer::json_compatible())
386 .map_err(|error| JsValue::from_str(&error.to_string()))
387 }
388
389 #[wasm_bindgen(js_name = closeIncoming)]
390 pub fn close_incoming(&self) {
391 self.inner.borrow_mut().close_incoming();
392 }
393
394 #[wasm_bindgen(js_name = closeOutgoing)]
395 pub fn close_outgoing(&self) {
396 self.inner.borrow_mut().close_outgoing();
397 }
398
399 pub fn close(&self) {
400 self.inner.borrow_mut().close();
401 }
402 }
403
404 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
405 fn emit_wasm_carrier_action(
406 handler: &Rc<RefCell<Option<js_sys::Function>>>,
407 action: serde_json::Value,
408 ) {
409 let Some(handler) = handler.borrow().as_ref().cloned() else {
410 return;
411 };
412 let Ok(value) = serde::Serialize::serialize(
418 &action,
419 &serde_wasm_bindgen::Serializer::json_compatible(),
420 ) else {
421 return;
422 };
423 if let Err(error) = handler.call1(&JsValue::UNDEFINED, &value) {
424 web_sys::console::error_2(
425 &JsValue::from_str("[OpenRTC][WASM carrier] action handler failed"),
426 &error,
427 );
428 }
429 }
430
431 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
432 fn wasm_carrier_failure_code(error: &anyhow::Error) -> &'static str {
433 let message = format!("{error:#}");
434 if message.contains("base generation changed before candidate acknowledgement")
435 || message.contains("incumbent generation changed")
436 || message.contains("replacement incumbent is stale")
437 {
438 "carrier-base-generation-stale"
439 } else if message.contains("authorization epoch changed") {
440 "carrier-authorization-stale"
441 } else if message.contains("stale before atomic commit")
442 || message.contains("became stale during atomic commit")
443 {
444 "carrier-logical-generation-stale"
445 } else if message.contains("attempt was retired") || message.contains("retired upgrade") {
446 "carrier-attempt-retired"
447 } else {
448 "carrier-proof-failed"
449 }
450 }
451
452 #[wasm_bindgen]
453 pub struct WasmClient {
454 inner: Arc<Client>,
455 portable_media: RefCell<crate::media::PortableMediaSession>,
456 broadcast_sessions: RefCell<HashMap<String, crate::broadcast::BroadcastSession>>,
457 broadcast_signers: RefCell<HashMap<String, crate::broadcast::BroadcastPublisherSigner>>,
458 identity_credential: Arc<Mutex<Option<String>>>,
459 last_auth_log: Arc<Mutex<Option<(bool, usize)>>>,
460 #[cfg(feature = "iroh-protocols-wasm")]
461 persistent_protocols:
462 Rc<tokio::sync::Mutex<Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>>>,
463 #[cfg(feature = "managed-group-encryption")]
464 managed_group_controllers:
465 Rc<RefCell<HashMap<String, crate::managed_group_controller::ManagedGroupController>>>,
466 #[cfg(feature = "transport-webrtc")]
467 wasm_webrtc_carrier_sessions:
468 Rc<RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>>,
469 #[cfg(feature = "transport-webrtc")]
470 wasm_webrtc_carrier_attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
471 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
472 wasm_carrier_action_handler: Rc<RefCell<Option<js_sys::Function>>>,
473 #[cfg(feature = "transport-moq")]
474 wasm_moq_carrier_sessions:
475 Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
476 #[cfg(feature = "transport-moq")]
477 wasm_moq_carrier_attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
478 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
479 wasm_remote_carrier_capabilities:
480 Rc<RefCell<HashMap<String, WasmRemoteCarrierCapabilities>>>,
481 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
487 wasm_carrier_peer_lifecycle: Rc<tokio::sync::Mutex<()>>,
488 }
489
490 #[cfg(feature = "transport-webrtc")]
491 async fn fail_wasm_webrtc_attempt(
492 inner: Arc<Client>,
493 attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
494 sessions: Rc<
495 RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>,
496 >,
497 handler: Rc<RefCell<Option<js_sys::Function>>>,
498 attempt: WasmWebRtcCarrierAttempt,
499 failure_code: &'static str,
500 notify_peer: bool,
501 ) {
502 let kind = crate::client::IrohPathKind::WebRtc;
503 if !inner
504 .retire_wasm_carrier_upgrade(
505 &attempt.connection_id,
506 kind,
507 &attempt.bootstrap.upgrade_id,
508 attempt.generation,
509 )
510 .await
511 {
512 return;
513 }
514 if attempts
515 .borrow()
516 .get(&attempt.connection_id)
517 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
518 {
519 attempts.borrow_mut().remove(&attempt.connection_id);
520 }
521 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
522 if notify_peer {
523 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
524 &attempt.bootstrap,
525 failure_code,
526 ) {
527 emit_wasm_carrier_action(
528 &handler,
529 serde_json::json!({
530 "type": "send-control",
531 "connectionId": attempt.connection_id,
532 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
533 "envelope": failed,
534 }),
535 );
536 }
537 }
538 emit_wasm_carrier_action(
539 &handler,
540 serde_json::json!({
541 "type": "retire-webrtc",
542 "connectionId": attempt.connection_id,
543 "upgradeId": attempt.bootstrap.upgrade_id,
544 "failureCode": failure_code,
545 }),
546 );
547 let retry_pending = attempt.retry_count == 0
548 && matches!(
549 failure_code,
550 "data-channel-failed"
551 | "ice-failed"
552 | "signaling-failed"
553 | "carrier-base-generation-stale"
554 );
555 if attempt.role == "initiator" && !retry_pending {
556 emit_wasm_carrier_action(
557 &handler,
558 serde_json::json!({
559 "type": "advance-carrier",
560 "connectionId": attempt.connection_id,
561 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
562 "failedRoute": "webrtc",
563 }),
564 );
565 }
566 }
567
568 #[cfg(feature = "transport-webrtc")]
569 async fn retire_selected_wasm_webrtc_carrier(
570 inner: Arc<Client>,
571 attempts: Rc<RefCell<HashMap<String, WasmWebRtcCarrierAttempt>>>,
572 sessions: Rc<
573 RefCell<HashMap<String, crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession>>,
574 >,
575 handler: Rc<RefCell<Option<js_sys::Function>>>,
576 attempt: &WasmWebRtcCarrierAttempt,
577 failure_code: &'static str,
578 lifecycle_reason: &'static str,
579 ) -> bool {
580 if !inner
581 .close_current_iroh_carrier_generation_with_reason(
582 &attempt.connection_id,
583 attempt.remote_endpoint_id,
584 crate::client::IrohPathKind::WebRtc,
585 attempt.generation.transport_generation.saturating_add(1),
586 lifecycle_reason,
587 )
588 .await
589 {
590 return false;
591 }
592 web_sys::console::warn_1(&JsValue::from_str(&format!(
593 "[OpenRTC][WebRTC carrier] selected mechanism ended connection_id={} upgrade_id={} failure_code={failure_code}",
594 attempt.connection_id, attempt.bootstrap.upgrade_id,
595 )));
596 if attempts
597 .borrow()
598 .get(&attempt.connection_id)
599 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
600 {
601 attempts.borrow_mut().remove(&attempt.connection_id);
602 }
603 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
604 emit_wasm_carrier_action(
605 &handler,
606 serde_json::json!({
607 "type": "retire-webrtc",
608 "connectionId": attempt.connection_id,
609 "upgradeId": attempt.bootstrap.upgrade_id,
610 "failureCode": failure_code,
611 }),
612 );
613 true
614 }
615
616 #[cfg(feature = "transport-webrtc")]
617 impl WasmClient {
618 fn schedule_wasm_webrtc_carrier_watchdog(
619 &self,
620 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
621 ) {
622 let inner = self.inner.clone();
623 let attempts = self.wasm_webrtc_carrier_attempts.clone();
624 let sessions = self.wasm_webrtc_carrier_sessions.clone();
625 let handler = self.wasm_carrier_action_handler.clone();
626 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
627 spawn_local(async move {
628 gloo_timers::future::sleep(std::time::Duration::from_secs(30)).await;
629 let _lifecycle = lifecycle.lock().await;
630 let attempt = attempts
631 .borrow()
632 .values()
633 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
634 .cloned();
635 if let Some(attempt) = attempt {
636 if !inner
637 .wasm_carrier_upgrade_is_current(
638 &attempt.connection_id,
639 crate::client::IrohPathKind::WebRtc,
640 &attempt.bootstrap.upgrade_id,
641 attempt.generation,
642 )
643 .await
644 {
645 return;
646 }
647 fail_wasm_webrtc_attempt(
648 inner,
649 attempts,
650 sessions,
651 handler,
652 attempt,
653 "carrier-timeout",
654 true,
655 )
656 .await;
657 }
658 });
659 }
660
661 async fn fail_wasm_webrtc_carrier_attempt(
662 &self,
663 attempt: WasmWebRtcCarrierAttempt,
664 failure_code: &'static str,
665 notify_peer: bool,
666 ) {
667 fail_wasm_webrtc_attempt(
668 self.inner.clone(),
669 self.wasm_webrtc_carrier_attempts.clone(),
670 self.wasm_webrtc_carrier_sessions.clone(),
671 self.wasm_carrier_action_handler.clone(),
672 attempt,
673 failure_code,
674 notify_peer,
675 )
676 .await;
677 }
678
679 fn spawn_outbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
680 let inner = self.inner.clone();
681 let attempts = self.wasm_webrtc_carrier_attempts.clone();
682 let sessions = self.wasm_webrtc_carrier_sessions.clone();
683 let handler = self.wasm_carrier_action_handler.clone();
684 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
685 spawn_local(async move {
686 let kind = crate::client::IrohPathKind::WebRtc;
687 let result = inner
688 .complete_outbound_wasm_carrier_upgrade(
689 &attempt.connection_id,
690 &attempt.remote_endpoint_id.to_string(),
691 &attempt.bootstrap.upgrade_id,
692 attempt.generation,
693 kind,
694 )
695 .await;
696 let _lifecycle = lifecycle.lock().await;
697 if let Err(error) = result {
698 let failure_code = wasm_carrier_failure_code(&error);
699 web_sys::console::error_1(&JsValue::from_str(&format!(
700 "[OpenRTC][WebRTC carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
701 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
702 )));
703 fail_wasm_webrtc_attempt(
704 inner,
705 attempts,
706 sessions,
707 handler,
708 attempt,
709 failure_code,
710 true,
711 )
712 .await;
713 return;
714 }
715 if !inner
716 .retire_wasm_carrier_upgrade(
717 &attempt.connection_id,
718 kind,
719 &attempt.bootstrap.upgrade_id,
720 attempt.generation,
721 )
722 .await
723 {
724 return;
725 }
726 emit_wasm_carrier_action(
727 &handler,
728 serde_json::json!({
729 "type": "selected",
730 "connectionId": attempt.connection_id,
731 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
732 "upgradeId": attempt.bootstrap.upgrade_id,
733 "family": "iroh",
734 "carrier": "webrtc",
735 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
736 "routeGeneration": 0,
737 }),
738 );
739 });
740 }
741
742 fn take_ready_outbound_wasm_webrtc_carrier_attempt(
743 &self,
744 connection_id: &str,
745 upgrade_id: &str,
746 ) -> Option<WasmWebRtcCarrierAttempt> {
747 if !self
748 .wasm_webrtc_carrier_sessions
749 .borrow()
750 .contains_key(upgrade_id)
751 {
752 return None;
753 }
754 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
755 let attempt = attempts.get_mut(connection_id).filter(|attempt| {
756 attempt.bootstrap.upgrade_id == upgrade_id
757 && attempt.role == "initiator"
758 && attempt.remote_ready
759 && !attempt.completion_started
760 })?;
761 attempt.completion_started = true;
762 Some(attempt.clone())
763 }
764
765 fn spawn_inbound_wasm_webrtc_carrier_completion(&self, attempt: WasmWebRtcCarrierAttempt) {
766 let inner = self.inner.clone();
767 let attempts = self.wasm_webrtc_carrier_attempts.clone();
768 let sessions = self.wasm_webrtc_carrier_sessions.clone();
769 let handler = self.wasm_carrier_action_handler.clone();
770 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
771 spawn_local(async move {
772 let kind = crate::client::IrohPathKind::WebRtc;
773 let node = inner.iroh_node.read().await.as_ref().cloned();
774 let result = async {
775 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
776 let authorization_expires_at_ms = attempt
777 .inbound_authorization_expires_at_ms
778 .ok_or_else(|| anyhow::anyhow!("browser WebRTC inbound authorization is missing"))?;
779 let candidate = node
780 .wait_for_inbound_replacement_candidate(
781 attempt.remote_endpoint_id,
782 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
783 authorization_expires_at_ms,
784 std::time::Duration::from_secs(40),
785 )
786 .await?;
787 let policy_epoch = inner
788 .require_current_wasm_carrier_upgrade_fence(
789 &attempt.connection_id,
790 kind,
791 &attempt.bootstrap.upgrade_id,
792 attempt.generation,
793 )
794 .await?;
795 let authorization_fence = inner
796 .capture_wasm_iroh_carrier_authorization_fence(
797 &attempt.connection_id,
798 attempt.generation,
799 kind,
800 policy_epoch,
801 )?;
802 let proof = inner.wasm_candidate_proof_probe(
803 &attempt.connection_id,
804 &attempt.bootstrap.upgrade_id,
805 attempt.bootstrap.base.transport_generation,
806 attempt.bootstrap.base.route_generation,
807 kind,
808 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
809 )?;
810 let (send, probe, ack) = inner
811 .receive_inbound_wasm_carrier_candidate_proof(
812 &attempt.connection_id,
813 &candidate,
814 &proof,
815 )
816 .await?;
817 anyhow::ensure!(
818 inner
819 .current_wasm_peer_data_generation(&attempt.connection_id, None)
820 .await
821 == Some(attempt.generation),
822 "browser WebRTC inbound carrier base generation changed before candidate acknowledgement"
823 );
824 inner
825 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
826 .await?;
827 let (commit_send, committed) = inner
828 .receive_inbound_wasm_carrier_commit(
829 &attempt.connection_id,
830 &candidate,
831 &probe,
832 )
833 .await?;
834 let committed_replacement = inner
835 .commit_proven_wasm_carrier_candidate(
836 &attempt.connection_id,
837 &attempt.bootstrap.upgrade_id,
838 attempt.generation,
839 kind,
840 authorization_fence,
841 candidate,
842 )
843 .await?;
844 inner
845 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
846 .await?;
847 let replacement_transport_stable_id = committed_replacement
848 .logical_result()
849 .transport_stable_id
850 .ok_or_else(|| {
851 anyhow::anyhow!(
852 "browser WebRTC replacement has no stable ID"
853 )
854 })?;
855 committed_replacement.finish(b"wasm-custom-transport-upgrade");
856 inner
857 .publish_committed_wasm_carrier_route(
858 &attempt.connection_id,
859 kind,
860 replacement_transport_stable_id,
861 )
862 .await?;
863 Ok::<(), anyhow::Error>(())
864 }
865 .await;
866 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
867 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
868 node.revoke_inbound_replacement_if_current(
869 attempt.remote_endpoint_id,
870 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
871 expires_at_ms,
872 )
873 .await;
874 }
875 }
876 let _lifecycle = lifecycle.lock().await;
877 if let Err(error) = result {
878 let failure_code = wasm_carrier_failure_code(&error);
879 web_sys::console::error_1(&JsValue::from_str(&format!(
880 "[OpenRTC][WebRTC carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
881 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
882 )));
883 fail_wasm_webrtc_attempt(
884 inner,
885 attempts,
886 sessions,
887 handler,
888 attempt,
889 failure_code,
890 true,
891 )
892 .await;
893 return;
894 }
895 if !inner
896 .retire_wasm_carrier_upgrade(
897 &attempt.connection_id,
898 kind,
899 &attempt.bootstrap.upgrade_id,
900 attempt.generation,
901 )
902 .await
903 {
904 return;
905 }
906 emit_wasm_carrier_action(
907 &handler,
908 serde_json::json!({
909 "type": "selected",
910 "connectionId": attempt.connection_id,
911 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
912 "upgradeId": attempt.bootstrap.upgrade_id,
913 "family": "iroh",
914 "carrier": "webrtc",
915 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
916 "routeGeneration": 0,
917 }),
918 );
919 });
920 }
921 }
922
923 #[cfg(feature = "transport-moq")]
924 fn wasm_moq_carrier_namespaces(
925 local_endpoint_id: &str,
926 remote_endpoint_id: &str,
927 carrier_session_id: &str,
928 ) -> (String, String, &'static str) {
929 let (first, second) = if local_endpoint_id <= remote_endpoint_id {
930 (local_endpoint_id, remote_endpoint_id)
931 } else {
932 (remote_endpoint_id, local_endpoint_id)
933 };
934 let base = format!("openrtc/iroh-carrier/moq/{first}/{second}/{carrier_session_id}");
935 (
936 format!("{base}/from/{local_endpoint_id}"),
937 format!("{base}/from/{remote_endpoint_id}"),
938 "iroh-packets",
939 )
940 }
941
942 #[cfg(feature = "transport-moq")]
943 async fn fail_wasm_moq_attempt(
944 inner: Arc<Client>,
945 attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
946 sessions: Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
947 handler: Rc<RefCell<Option<js_sys::Function>>>,
948 attempt: WasmMoqCarrierAttempt,
949 failure_code: &'static str,
950 notify_peer: bool,
951 ) {
952 let kind = crate::client::IrohPathKind::Moq;
953 if !inner
954 .retire_wasm_carrier_upgrade(
955 &attempt.connection_id,
956 kind,
957 &attempt.bootstrap.upgrade_id,
958 attempt.generation,
959 )
960 .await
961 {
962 return;
963 }
964 if attempts
965 .borrow()
966 .get(&attempt.connection_id)
967 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
968 {
969 attempts.borrow_mut().remove(&attempt.connection_id);
970 }
971 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
972 if notify_peer {
973 if let Ok(failed) = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
974 &attempt.bootstrap,
975 failure_code,
976 ) {
977 emit_wasm_carrier_action(
978 &handler,
979 serde_json::json!({
980 "type": "send-control",
981 "connectionId": attempt.connection_id,
982 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
983 "envelope": failed,
984 }),
985 );
986 }
987 }
988 emit_wasm_carrier_action(
989 &handler,
990 serde_json::json!({
991 "type": "retire-moq",
992 "connectionId": attempt.connection_id,
993 "upgradeId": attempt.bootstrap.upgrade_id,
994 "failureCode": failure_code,
995 }),
996 );
997 let retry_pending = attempt.role == "initiator"
998 && attempt.retry_count == 0
999 && failure_code == "carrier-base-generation-stale";
1000 if attempt.role == "initiator" && !retry_pending {
1001 emit_wasm_carrier_action(
1002 &handler,
1003 serde_json::json!({
1004 "type": "advance-carrier",
1005 "connectionId": attempt.connection_id,
1006 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1007 "failedRoute": "moq",
1008 }),
1009 );
1010 }
1011 }
1012
1013 #[cfg(feature = "transport-moq")]
1014 async fn retire_selected_wasm_moq_carrier(
1015 inner: Arc<Client>,
1016 attempts: Rc<RefCell<HashMap<String, WasmMoqCarrierAttempt>>>,
1017 sessions: Rc<RefCell<HashMap<String, crate::wasm_moq_carrier::WasmMoqCarrierSession>>>,
1018 handler: Rc<RefCell<Option<js_sys::Function>>>,
1019 attempt: &WasmMoqCarrierAttempt,
1020 failure_code: &'static str,
1021 lifecycle_reason: &'static str,
1022 ) -> bool {
1023 if !inner
1024 .close_current_iroh_carrier_generation_with_reason(
1025 &attempt.connection_id,
1026 attempt.remote_endpoint_id,
1027 crate::client::IrohPathKind::Moq,
1028 attempt.generation.transport_generation.saturating_add(1),
1029 lifecycle_reason,
1030 )
1031 .await
1032 {
1033 return false;
1034 }
1035 web_sys::console::warn_1(&JsValue::from_str(&format!(
1036 "[OpenRTC][MoQ carrier] selected mechanism ended connection_id={} upgrade_id={} failure_code={failure_code}",
1037 attempt.connection_id, attempt.bootstrap.upgrade_id,
1038 )));
1039 if attempts
1040 .borrow()
1041 .get(&attempt.connection_id)
1042 .is_some_and(|current| current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id)
1043 {
1044 attempts.borrow_mut().remove(&attempt.connection_id);
1045 }
1046 sessions.borrow_mut().remove(&attempt.bootstrap.upgrade_id);
1047 emit_wasm_carrier_action(
1048 &handler,
1049 serde_json::json!({
1050 "type": "retire-moq",
1051 "connectionId": attempt.connection_id,
1052 "upgradeId": attempt.bootstrap.upgrade_id,
1053 "failureCode": failure_code,
1054 }),
1055 );
1056 true
1057 }
1058
1059 #[cfg(feature = "transport-moq")]
1060 impl WasmClient {
1061 fn schedule_wasm_moq_carrier_watchdog(
1062 &self,
1063 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1064 ) {
1065 let inner = self.inner.clone();
1066 let attempts = self.wasm_moq_carrier_attempts.clone();
1067 let sessions = self.wasm_moq_carrier_sessions.clone();
1068 let handler = self.wasm_carrier_action_handler.clone();
1069 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1070 spawn_local(async move {
1071 gloo_timers::future::sleep(std::time::Duration::from_secs(30)).await;
1072 let _lifecycle = lifecycle.lock().await;
1073 let attempt = attempts
1074 .borrow()
1075 .values()
1076 .find(|attempt| attempt.bootstrap.upgrade_id == bootstrap.upgrade_id)
1077 .cloned();
1078 if let Some(attempt) = attempt {
1079 if !inner
1080 .wasm_carrier_upgrade_is_current(
1081 &attempt.connection_id,
1082 crate::client::IrohPathKind::Moq,
1083 &attempt.bootstrap.upgrade_id,
1084 attempt.generation,
1085 )
1086 .await
1087 {
1088 return;
1089 }
1090 fail_wasm_moq_attempt(
1091 inner,
1092 attempts,
1093 sessions,
1094 handler,
1095 attempt,
1096 "carrier-timeout",
1097 true,
1098 )
1099 .await;
1100 }
1101 });
1102 }
1103
1104 async fn fail_wasm_moq_carrier_attempt(
1105 &self,
1106 attempt: WasmMoqCarrierAttempt,
1107 failure_code: &'static str,
1108 notify_peer: bool,
1109 ) {
1110 fail_wasm_moq_attempt(
1111 self.inner.clone(),
1112 self.wasm_moq_carrier_attempts.clone(),
1113 self.wasm_moq_carrier_sessions.clone(),
1114 self.wasm_carrier_action_handler.clone(),
1115 attempt,
1116 failure_code,
1117 notify_peer,
1118 )
1119 .await;
1120 }
1121
1122 fn spawn_outbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
1123 let inner = self.inner.clone();
1124 let attempts = self.wasm_moq_carrier_attempts.clone();
1125 let sessions = self.wasm_moq_carrier_sessions.clone();
1126 let handler = self.wasm_carrier_action_handler.clone();
1127 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1128 spawn_local(async move {
1129 let kind = crate::client::IrohPathKind::Moq;
1130 let result = inner
1131 .complete_outbound_wasm_carrier_upgrade(
1132 &attempt.connection_id,
1133 &attempt.remote_endpoint_id.to_string(),
1134 &attempt.bootstrap.upgrade_id,
1135 attempt.generation,
1136 kind,
1137 )
1138 .await;
1139 let _lifecycle = lifecycle.lock().await;
1140 if let Err(error) = result {
1141 let failure_code = wasm_carrier_failure_code(&error);
1142 web_sys::console::error_1(&JsValue::from_str(&format!(
1143 "[OpenRTC][MoQ carrier] outbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
1144 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
1145 )));
1146 fail_wasm_moq_attempt(
1147 inner,
1148 attempts,
1149 sessions,
1150 handler,
1151 attempt,
1152 failure_code,
1153 true,
1154 )
1155 .await;
1156 return;
1157 }
1158 if !inner
1159 .retire_wasm_carrier_upgrade(
1160 &attempt.connection_id,
1161 kind,
1162 &attempt.bootstrap.upgrade_id,
1163 attempt.generation,
1164 )
1165 .await
1166 {
1167 return;
1168 }
1169 emit_wasm_carrier_action(
1170 &handler,
1171 serde_json::json!({
1172 "type": "selected",
1173 "connectionId": attempt.connection_id,
1174 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1175 "upgradeId": attempt.bootstrap.upgrade_id,
1176 "family": "iroh",
1177 "carrier": "moq",
1178 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
1179 "routeGeneration": 0,
1180 }),
1181 );
1182 });
1183 }
1184
1185 fn spawn_inbound_wasm_moq_carrier_completion(&self, attempt: WasmMoqCarrierAttempt) {
1186 let inner = self.inner.clone();
1187 let attempts = self.wasm_moq_carrier_attempts.clone();
1188 let sessions = self.wasm_moq_carrier_sessions.clone();
1189 let handler = self.wasm_carrier_action_handler.clone();
1190 let lifecycle = self.wasm_carrier_peer_lifecycle.clone();
1191 spawn_local(async move {
1192 let kind = crate::client::IrohPathKind::Moq;
1193 let node = inner.iroh_node.read().await.as_ref().cloned();
1194 let result = async {
1195 let node = node.ok_or_else(|| anyhow::anyhow!("Iroh node is unavailable"))?;
1196 let authorization_expires_at_ms = attempt
1197 .inbound_authorization_expires_at_ms
1198 .ok_or_else(|| anyhow::anyhow!("browser MoQ inbound authorization is missing"))?;
1199 let candidate = node
1200 .wait_for_inbound_replacement_candidate(
1201 attempt.remote_endpoint_id,
1202 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1203 authorization_expires_at_ms,
1204 std::time::Duration::from_secs(40),
1205 )
1206 .await?;
1207 let policy_epoch = inner
1208 .require_current_wasm_carrier_upgrade_fence(
1209 &attempt.connection_id,
1210 kind,
1211 &attempt.bootstrap.upgrade_id,
1212 attempt.generation,
1213 )
1214 .await?;
1215 let authorization_fence = inner
1216 .capture_wasm_iroh_carrier_authorization_fence(
1217 &attempt.connection_id,
1218 attempt.generation,
1219 kind,
1220 policy_epoch,
1221 )?;
1222 let proof = inner.wasm_candidate_proof_probe(
1223 &attempt.connection_id,
1224 &attempt.bootstrap.upgrade_id,
1225 attempt.bootstrap.base.transport_generation,
1226 attempt.bootstrap.base.route_generation,
1227 kind,
1228 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1229 )?;
1230 let (send, probe, ack) = inner
1231 .receive_inbound_wasm_carrier_candidate_proof(
1232 &attempt.connection_id,
1233 &candidate,
1234 &proof,
1235 )
1236 .await?;
1237 anyhow::ensure!(
1238 inner
1239 .current_wasm_peer_data_generation(&attempt.connection_id, None)
1240 .await
1241 == Some(attempt.generation),
1242 "browser MoQ inbound carrier base generation changed before candidate acknowledgement"
1243 );
1244 inner
1245 .send_inbound_wasm_carrier_candidate_ack(send, &ack)
1246 .await?;
1247 let (commit_send, committed) = inner
1248 .receive_inbound_wasm_carrier_commit(
1249 &attempt.connection_id,
1250 &candidate,
1251 &probe,
1252 )
1253 .await?;
1254 let committed_replacement = inner
1255 .commit_proven_wasm_carrier_candidate(
1256 &attempt.connection_id,
1257 &attempt.bootstrap.upgrade_id,
1258 attempt.generation,
1259 kind,
1260 authorization_fence,
1261 candidate,
1262 )
1263 .await?;
1264 inner
1265 .send_inbound_wasm_carrier_candidate_ack(commit_send, &committed)
1266 .await?;
1267 let replacement_transport_stable_id = committed_replacement
1268 .logical_result()
1269 .transport_stable_id
1270 .ok_or_else(|| {
1271 anyhow::anyhow!("browser MoQ replacement has no stable ID")
1272 })?;
1273 committed_replacement.finish(b"wasm-custom-transport-upgrade");
1274 inner
1275 .publish_committed_wasm_carrier_route(
1276 &attempt.connection_id,
1277 kind,
1278 replacement_transport_stable_id,
1279 )
1280 .await?;
1281 Ok::<(), anyhow::Error>(())
1282 }
1283 .await;
1284 if let Some(expires_at_ms) = attempt.inbound_authorization_expires_at_ms {
1285 if let Some(node) = inner.iroh_node.read().await.as_ref().cloned() {
1286 node.revoke_inbound_replacement_if_current(
1287 attempt.remote_endpoint_id,
1288 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1289 expires_at_ms,
1290 )
1291 .await;
1292 }
1293 }
1294 let _lifecycle = lifecycle.lock().await;
1295 if let Err(error) = result {
1296 let failure_code = wasm_carrier_failure_code(&error);
1297 web_sys::console::error_1(&JsValue::from_str(&format!(
1298 "[OpenRTC][MoQ carrier] inbound candidate completion failed connection_id={} upgrade_id={} failure_code={} error={error:#}",
1299 attempt.connection_id, attempt.bootstrap.upgrade_id, failure_code,
1300 )));
1301 fail_wasm_moq_attempt(
1302 inner,
1303 attempts,
1304 sessions,
1305 handler,
1306 attempt,
1307 failure_code,
1308 true,
1309 )
1310 .await;
1311 return;
1312 }
1313 if !inner
1314 .retire_wasm_carrier_upgrade(
1315 &attempt.connection_id,
1316 kind,
1317 &attempt.bootstrap.upgrade_id,
1318 attempt.generation,
1319 )
1320 .await
1321 {
1322 return;
1323 }
1324 emit_wasm_carrier_action(
1325 &handler,
1326 serde_json::json!({
1327 "type": "selected",
1328 "connectionId": attempt.connection_id,
1329 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
1330 "upgradeId": attempt.bootstrap.upgrade_id,
1331 "family": "iroh",
1332 "carrier": "moq",
1333 "transportGeneration": attempt.generation.transport_generation.saturating_add(1),
1334 "routeGeneration": 0,
1335 }),
1336 );
1337 });
1338 }
1339
1340 async fn handle_wasm_moq_bootstrap(
1341 &self,
1342 connection_id: String,
1343 remote_endpoint_id: String,
1344 bootstrap: crate::iroh_carrier_bootstrap::CarrierBootstrapFrame,
1345 ) -> Result<bool, JsValue> {
1346 let endpoint_id = remote_endpoint_id
1347 .parse::<iroh::EndpointId>()
1348 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1349 let kind = crate::client::IrohPathKind::Moq;
1350 match bootstrap.action {
1351 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
1352 let local_endpoint_id = self
1353 .inner
1354 .current_node_id()
1355 .await
1356 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
1357 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
1358 || !self.inner.is_moq_carrier_enabled().await
1359 {
1360 return Ok(true);
1361 }
1362 if !crate::iroh_connection_policy::custom_carrier_base_allows(
1363 self.inner.iroh_path_kind(&remote_endpoint_id).await,
1364 crate::client::IrohPathKind::Moq,
1365 false,
1366 ) {
1367 return Ok(true);
1368 }
1369 let base_connection = self
1370 .inner
1371 .get_connection(endpoint_id)
1372 .await
1373 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
1374 let generation = self
1375 .inner
1376 .current_wasm_peer_data_generation(&connection_id, None)
1377 .await
1378 .ok_or_else(|| {
1379 JsValue::from_str("MoQ carrier generation is unavailable")
1380 })?;
1381 if crate::transport_generation::for_connection(&base_connection)
1382 != generation.transport_stable_id
1383 {
1384 let failed =
1385 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
1386 &bootstrap,
1387 "carrier-base-generation-stale",
1388 )
1389 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1390 emit_wasm_carrier_action(
1391 &self.wasm_carrier_action_handler,
1392 serde_json::json!({
1393 "type": "send-control",
1394 "connectionId": connection_id,
1395 "remoteEndpointId": remote_endpoint_id,
1396 "envelope": failed,
1397 }),
1398 );
1399 return Ok(true);
1400 }
1401 if !matches!(
1402 self.inner
1403 .reserve_wasm_carrier_upgrade(
1404 &connection_id,
1405 kind,
1406 &bootstrap.upgrade_id,
1407 generation,
1408 )
1409 .await,
1410 crate::client::WasmCarrierUpgradeReservation::Reserved
1411 ) {
1412 return Ok(true);
1413 }
1414 let node = self
1415 .inner
1416 .iroh_node
1417 .read()
1418 .await
1419 .as_ref()
1420 .cloned()
1421 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
1422 let authorization_expiry = node
1423 .authorize_pending_inbound_replacement(
1424 endpoint_id,
1425 crate::iroh_carrier_kind::EXPERIMENTAL_MOQ_TRANSPORT_ID,
1426 std::time::Duration::from_secs(45),
1427 )
1428 .await;
1429 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
1430 connection_id.clone(),
1431 WasmMoqCarrierAttempt {
1432 connection_id: connection_id.clone(),
1433 remote_endpoint_id: endpoint_id,
1434 bootstrap: bootstrap.clone(),
1435 generation,
1436 role: "responder",
1437 prepared: false,
1438 retry_sent: false,
1439 retry_count: bootstrap.attempt.saturating_sub(1),
1440 inbound_authorization_expires_at_ms: Some(authorization_expiry),
1441 },
1442 );
1443 if let Some(previous) = previous {
1444 self.wasm_moq_carrier_sessions
1445 .borrow_mut()
1446 .remove(&previous.bootstrap.upgrade_id);
1447 }
1448 let (publish_namespace, subscribe_namespace, track_name) =
1449 wasm_moq_carrier_namespaces(
1450 &local_endpoint_id,
1451 &remote_endpoint_id,
1452 &bootstrap.carrier_session_id,
1453 );
1454 emit_wasm_carrier_action(
1455 &self.wasm_carrier_action_handler,
1456 serde_json::json!({
1457 "type": "prepare-moq",
1458 "connectionId": connection_id,
1459 "remoteEndpointId": remote_endpoint_id,
1460 "role": "responder",
1461 "upgradeId": bootstrap.upgrade_id,
1462 "carrierSessionId": bootstrap.carrier_session_id,
1463 "transportGeneration": generation.transport_generation.saturating_add(1),
1464 "publishNamespace": publish_namespace,
1465 "subscribeNamespace": subscribe_namespace,
1466 "trackName": track_name,
1467 }),
1468 );
1469 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
1470 }
1471 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
1472 let attempt = self
1473 .wasm_moq_carrier_attempts
1474 .borrow()
1475 .get(&connection_id)
1476 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1477 .cloned();
1478 if let Some(attempt) = attempt {
1479 emit_wasm_carrier_action(
1480 &self.wasm_carrier_action_handler,
1481 serde_json::json!({
1482 "type": "activate-moq",
1483 "connectionId": attempt.connection_id,
1484 "upgradeId": attempt.bootstrap.upgrade_id,
1485 }),
1486 );
1487 }
1488 }
1489 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
1490 let attempt = self
1491 .wasm_moq_carrier_attempts
1492 .borrow()
1493 .get(&connection_id)
1494 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
1495 .cloned();
1496 if let Some(attempt) = attempt {
1497 let failure_code = crate::client::wasm_peer_carrier_failure_code(
1498 bootstrap.failure_code.as_deref(),
1499 );
1500 if crate::client::should_rearm_wasm_carrier_event_retry(
1501 failure_code,
1502 attempt.role == "initiator",
1503 false,
1504 ) {
1505 if let Some(current) = self
1506 .wasm_moq_carrier_attempts
1507 .borrow_mut()
1508 .get_mut(&connection_id)
1509 .filter(|current| {
1510 current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id
1511 })
1512 {
1513 current.retry_sent = false;
1519 }
1520 return Ok(true);
1521 }
1522 let should_retry = attempt.role == "initiator"
1523 && attempt.retry_count == 0
1524 && failure_code == "carrier-base-generation-stale";
1525 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
1526 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, false)
1527 .await;
1528 if should_retry {
1529 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
1533 let _ = self
1534 .begin_iroh_moq_carrier_attempt(
1535 connection_id,
1536 remote_endpoint_id,
1537 1,
1538 )
1539 .await;
1540 }
1541 }
1542 }
1543 }
1544 Ok(true)
1545 }
1546 }
1547
1548 #[wasm_bindgen]
1549 impl WasmClient {
1550 #[wasm_bindgen(constructor)]
1554 pub fn new(api_key: String) -> Result<WasmClient, JsValue> {
1555 let api_key = crate::validate_api_key(&api_key)
1556 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1557 let app_tag = crate::app_tag_from_api_key(api_key);
1558 let identity_credential: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
1559 let credential_state = identity_credential.clone();
1560 let identity_credential_provider = Box::new(move || -> Option<String> {
1561 credential_state.lock().ok().and_then(|guard| guard.clone())
1562 });
1563
1564 Ok(Self {
1565 inner: Arc::new(Client::new_provider_neutral(
1566 app_tag,
1567 identity_credential_provider,
1568 )),
1569 portable_media: RefCell::new(crate::media::PortableMediaSession::default()),
1570 broadcast_sessions: RefCell::new(HashMap::new()),
1571 broadcast_signers: RefCell::new(HashMap::new()),
1572 identity_credential,
1573 last_auth_log: Arc::new(Mutex::new(None)),
1574 #[cfg(feature = "iroh-protocols-wasm")]
1575 persistent_protocols: Rc::new(tokio::sync::Mutex::new(None)),
1576 #[cfg(feature = "managed-group-encryption")]
1577 managed_group_controllers: Rc::new(RefCell::new(HashMap::new())),
1578 #[cfg(feature = "transport-webrtc")]
1579 wasm_webrtc_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1580 #[cfg(feature = "transport-webrtc")]
1581 wasm_webrtc_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1582 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1583 wasm_carrier_action_handler: Rc::new(RefCell::new(None)),
1584 #[cfg(feature = "transport-moq")]
1585 wasm_moq_carrier_sessions: Rc::new(RefCell::new(HashMap::new())),
1586 #[cfg(feature = "transport-moq")]
1587 wasm_moq_carrier_attempts: Rc::new(RefCell::new(HashMap::new())),
1588 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1589 wasm_remote_carrier_capabilities: Rc::new(RefCell::new(HashMap::new())),
1590 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
1591 wasm_carrier_peer_lifecycle: Rc::new(tokio::sync::Mutex::new(())),
1592 })
1593 }
1594
1595 #[cfg(feature = "managed-group-encryption")]
1599 #[wasm_bindgen(js_name = __initManagedRoomGroup)]
1600 pub fn init_managed_room_group(
1601 &self,
1602 avenue_key: String,
1603 device_id: String,
1604 wrapping_key: Vec<u8>,
1605 sealed_state: Option<Vec<u8>>,
1606 ) -> Result<JsValue, JsValue> {
1607 let wrapping_key: [u8; 32] = wrapping_key
1608 .try_into()
1609 .map_err(|_| JsValue::from_str("managed room wrapping key must be 32 bytes"))?;
1610 let mut controllers = self.managed_group_controllers.borrow_mut();
1611 if !controllers.contains_key(&avenue_key) {
1612 let controller = crate::managed_group_controller::ManagedGroupController::new(
1613 device_id,
1614 wrapping_key,
1615 sealed_state.as_deref(),
1616 )
1617 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1618 controllers.insert(avenue_key.clone(), controller);
1619 }
1620 let controller = controllers
1621 .get(&avenue_key)
1622 .ok_or_else(|| JsValue::from_str("managed room group initialization failed"))?;
1623 let action = controller
1624 .publish_key_package()
1625 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1626 serde::Serialize::serialize(&action, &serde_wasm_bindgen::Serializer::json_compatible())
1627 .map_err(|error| JsValue::from_str(&error.to_string()))
1628 }
1629
1630 #[cfg(feature = "managed-group-encryption")]
1631 #[wasm_bindgen(js_name = __handleManagedRoomPreparePage)]
1632 pub fn handle_managed_room_prepare_page(
1633 &self,
1634 avenue_key: String,
1635 page: JsValue,
1636 ) -> Result<JsValue, JsValue> {
1637 let page = serde_wasm_bindgen::from_value(page)
1638 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1639 let mut controllers = self.managed_group_controllers.borrow_mut();
1640 let controller = controllers
1641 .get_mut(&avenue_key)
1642 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?;
1643 let actions = controller
1644 .handle_prepare_page(page)
1645 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1646 serde::Serialize::serialize(
1647 &actions,
1648 &serde_wasm_bindgen::Serializer::json_compatible(),
1649 )
1650 .map_err(|error| JsValue::from_str(&error.to_string()))
1651 }
1652
1653 #[cfg(feature = "managed-group-encryption")]
1654 #[wasm_bindgen(js_name = __handleManagedRoomArtifactChunk)]
1655 pub fn handle_managed_room_artifact_chunk(
1656 &self,
1657 avenue_key: String,
1658 chunk: JsValue,
1659 ) -> Result<JsValue, JsValue> {
1660 let chunk = serde_wasm_bindgen::from_value(chunk)
1661 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1662 let mut controllers = self.managed_group_controllers.borrow_mut();
1663 let controller = controllers
1664 .get_mut(&avenue_key)
1665 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?;
1666 let actions = controller
1667 .handle_artifact_chunk(chunk)
1668 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1669 serde::Serialize::serialize(
1670 &actions,
1671 &serde_wasm_bindgen::Serializer::json_compatible(),
1672 )
1673 .map_err(|error| JsValue::from_str(&error.to_string()))
1674 }
1675
1676 #[cfg(feature = "managed-group-encryption")]
1677 #[wasm_bindgen(js_name = __sealManagedRoomPayload)]
1678 #[allow(clippy::too_many_arguments)]
1679 pub fn seal_managed_room_payload(
1680 &self,
1681 avenue_key: String,
1682 architecture_epoch: u64,
1683 encryption_epoch: u64,
1684 message_id: String,
1685 channel: String,
1686 priority: u8,
1687 zone_id: Option<String>,
1688 payload: Vec<u8>,
1689 ) -> Result<JsValue, JsValue> {
1690 let protected = self
1691 .managed_group_controllers
1692 .borrow_mut()
1693 .get_mut(&avenue_key)
1694 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?
1695 .seal_payload(
1696 architecture_epoch,
1697 encryption_epoch,
1698 &message_id,
1699 &channel,
1700 priority,
1701 zone_id.as_deref(),
1702 &payload,
1703 )
1704 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1705 serde_wasm_bindgen::to_value(&serde_json::json!({
1706 "payload": base64::Engine::encode(
1707 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1708 protected.data,
1709 ),
1710 "sealedState": base64::Engine::encode(
1711 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1712 protected.sealed_state,
1713 ),
1714 }))
1715 .map_err(|error| JsValue::from_str(&error.to_string()))
1716 }
1717
1718 #[cfg(feature = "managed-group-encryption")]
1719 #[wasm_bindgen(js_name = __openManagedRoomPayload)]
1720 #[allow(clippy::too_many_arguments)]
1721 pub fn open_managed_room_payload(
1722 &self,
1723 avenue_key: String,
1724 architecture_epoch: u64,
1725 encryption_epoch: u64,
1726 message_id: String,
1727 channel: String,
1728 priority: u8,
1729 zone_id: Option<String>,
1730 ciphertext: Vec<u8>,
1731 ) -> Result<JsValue, JsValue> {
1732 let protected = self
1733 .managed_group_controllers
1734 .borrow_mut()
1735 .get_mut(&avenue_key)
1736 .ok_or_else(|| JsValue::from_str("managed room group is not initialized"))?
1737 .open_payload(
1738 architecture_epoch,
1739 encryption_epoch,
1740 &message_id,
1741 &channel,
1742 priority,
1743 zone_id.as_deref(),
1744 &ciphertext,
1745 )
1746 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
1747 serde_wasm_bindgen::to_value(&serde_json::json!({
1748 "payload": base64::Engine::encode(
1749 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1750 protected.data,
1751 ),
1752 "sealedState": base64::Engine::encode(
1753 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
1754 protected.sealed_state,
1755 ),
1756 }))
1757 .map_err(|error| JsValue::from_str(&error.to_string()))
1758 }
1759
1760 #[cfg(feature = "managed-group-encryption")]
1761 #[wasm_bindgen(js_name = __forgetManagedRoomGroup)]
1762 pub fn forget_managed_room_group(&self, avenue_key: String) {
1763 self.managed_group_controllers
1764 .borrow_mut()
1765 .remove(&avenue_key);
1766 }
1767
1768 #[wasm_bindgen(js_name = __prepareOpenRtcBroadcastGrant)]
1772 pub fn prepare_openrtc_broadcast_grant(
1773 &self,
1774 grant_token: String,
1775 issuer_public_key: Vec<u8>,
1776 now_ms: f64,
1777 ) -> Result<JsValue, JsValue> {
1778 let issuer_public_key: [u8; 32] = issuer_public_key
1779 .try_into()
1780 .map_err(|_| JsValue::from_str("broadcast issuer key must be 32 bytes"))?;
1781 let issuer = ed25519_dalek::VerifyingKey::from_bytes(&issuer_public_key)
1782 .map_err(|_| JsValue::from_str("broadcast issuer key is invalid"))?;
1783 if !now_ms.is_finite()
1784 || now_ms < 0.0
1785 || now_ms.fract() != 0.0
1786 || now_ms > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
1787 {
1788 return Err(JsValue::from_str(
1789 "broadcast now must be a non-negative JavaScript-safe integer",
1790 ));
1791 }
1792 let challenge = self
1793 .inner
1794 .broadcasts()
1795 .prepare_grant_verification(&grant_token, &issuer, now_ms as u64)
1796 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1797 let object = js_sys::Object::new();
1798 js_sys::Reflect::set(
1799 &object,
1800 &JsValue::from_str("handle"),
1801 &JsValue::from_str(&challenge.handle),
1802 )?;
1803 js_sys::Reflect::set(
1804 &object,
1805 &JsValue::from_str("signingBytes"),
1806 &js_sys::Uint8Array::from(challenge.signing_bytes.as_slice()),
1807 )?;
1808 js_sys::Reflect::set(
1809 &object,
1810 &JsValue::from_str("expiresAtMs"),
1811 &JsValue::from_f64(challenge.expires_at_ms as f64),
1812 )?;
1813 Ok(object.into())
1814 }
1815
1816 #[wasm_bindgen(js_name = __openOpenRtcBroadcast)]
1820 pub fn open_openrtc_broadcast(
1821 &self,
1822 grant_token: String,
1823 issuer_public_key: Vec<u8>,
1824 binding_challenge_handle: String,
1825 binding_signature: Vec<u8>,
1826 publication_signer_handle: Option<String>,
1827 now_ms: f64,
1828 ) -> Result<JsValue, JsValue> {
1829 let issuer_public_key: [u8; 32] = issuer_public_key
1830 .try_into()
1831 .map_err(|_| JsValue::from_str("broadcast issuer key must be 32 bytes"))?;
1832 let issuer = ed25519_dalek::VerifyingKey::from_bytes(&issuer_public_key)
1833 .map_err(|_| JsValue::from_str("broadcast issuer key is invalid"))?;
1834 if !now_ms.is_finite()
1835 || now_ms < 0.0
1836 || now_ms.fract() != 0.0
1837 || now_ms > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
1838 {
1839 return Err(JsValue::from_str(
1840 "broadcast now must be a non-negative JavaScript-safe integer",
1841 ));
1842 }
1843 let now_ms = now_ms as u64;
1844 let grant = self
1845 .inner
1846 .broadcasts()
1847 .complete_grant_verification(
1848 &grant_token,
1849 &issuer,
1850 &binding_challenge_handle,
1851 &binding_signature,
1852 now_ms,
1853 )
1854 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1855 let grant_generation = grant.grant_generation();
1856 let session = match publication_signer_handle {
1857 Some(handle) => {
1858 let signers = self.broadcast_signers.borrow();
1859 let signer = signers
1860 .get(&handle)
1861 .ok_or_else(|| JsValue::from_str("broadcast signer handle is invalid"))?;
1862 self.inner
1863 .broadcasts()
1864 .open_publisher(grant, signer, now_ms)
1865 }
1866 None => self.inner.broadcasts().open(grant, now_ms),
1867 }
1868 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1869 let handle_id = format!("{}:{grant_generation}", session.id());
1870 let result = serde_json::json!({
1871 "id": session.id(),
1872 "handleId": handle_id,
1873 "role": session.role(),
1874 "state": session.state(),
1875 "budget": session.budget(),
1876 "sourceSlots": session.source_slots(),
1877 });
1878 self.broadcast_sessions
1879 .borrow_mut()
1880 .insert(handle_id, session);
1881 serde::Serialize::serialize(&result, &serde_wasm_bindgen::Serializer::json_compatible())
1882 .map_err(|error| JsValue::from_str(&error.to_string()))
1883 }
1884
1885 #[wasm_bindgen(js_name = __createOpenRtcBroadcastPublisherSigner)]
1889 pub fn create_openrtc_broadcast_publisher_signer(&self) -> Result<JsValue, JsValue> {
1890 let signer = crate::broadcast::BroadcastPublisherSigner::generate()
1891 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1892 let mut handle_bytes = [0_u8; 16];
1893 getrandom::getrandom(&mut handle_bytes)
1894 .map_err(|_| JsValue::from_str("broadcast signer handle entropy failed"))?;
1895 let handle = hex::encode(handle_bytes);
1896 let public_key = signer.verifying_key();
1897 let object = js_sys::Object::new();
1898 js_sys::Reflect::set(
1899 &object,
1900 &JsValue::from_str("handle"),
1901 &JsValue::from_str(&handle),
1902 )?;
1903 js_sys::Reflect::set(
1904 &object,
1905 &JsValue::from_str("publicKey"),
1906 &js_sys::Uint8Array::from(public_key.as_slice()),
1907 )?;
1908 self.broadcast_signers.borrow_mut().insert(handle, signer);
1909 Ok(object.into())
1910 }
1911
1912 #[wasm_bindgen(js_name = __releaseOpenRtcBroadcastPublisherSigner)]
1913 pub fn release_openrtc_broadcast_publisher_signer(&self, handle: String) -> bool {
1914 self.broadcast_signers
1915 .borrow_mut()
1916 .remove(&handle)
1917 .is_some()
1918 }
1919
1920 #[wasm_bindgen(js_name = __takeOpenRtcBroadcastActions)]
1921 pub fn take_openrtc_broadcast_actions(
1922 &self,
1923 handle_id: String,
1924 max: usize,
1925 ) -> Result<JsValue, JsValue> {
1926 let sessions = self.broadcast_sessions.borrow();
1927 let session = sessions
1928 .get(&handle_id)
1929 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?;
1930 serde::Serialize::serialize(
1931 &session.take_actions(max),
1932 &serde_wasm_bindgen::Serializer::json_compatible(),
1933 )
1934 .map_err(|error| JsValue::from_str(&error.to_string()))
1935 }
1936
1937 #[wasm_bindgen(js_name = __getOpenRtcBroadcastStats)]
1938 pub fn get_openrtc_broadcast_stats(&self, handle_id: String) -> Result<JsValue, JsValue> {
1939 let sessions = self.broadcast_sessions.borrow();
1940 let stats = sessions
1941 .get(&handle_id)
1942 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
1943 .stats();
1944 serde::Serialize::serialize(&stats, &serde_wasm_bindgen::Serializer::json_compatible())
1945 .map_err(|error| JsValue::from_str(&error.to_string()))
1946 }
1947
1948 #[wasm_bindgen(js_name = __observeOpenRtcBroadcastAdapter)]
1949 pub fn observe_openrtc_broadcast_adapter(
1950 &self,
1951 handle_id: String,
1952 observation: JsValue,
1953 ) -> Result<(), JsValue> {
1954 let observation = serde_wasm_bindgen::from_value(observation)
1955 .map_err(|error| JsValue::from_str(&error.to_string()))?;
1956 self.broadcast_sessions
1957 .borrow()
1958 .get(&handle_id)
1959 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
1960 .observe(observation)
1961 .map_err(|error| JsValue::from_str(&error.to_string()))
1962 }
1963
1964 #[wasm_bindgen(js_name = __closeOpenRtcBroadcast)]
1965 pub fn close_openrtc_broadcast(&self, handle_id: String) -> Result<(), JsValue> {
1966 let session = self
1967 .broadcast_sessions
1968 .borrow_mut()
1969 .remove(&handle_id)
1970 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?;
1971 session.close();
1972 Ok(())
1973 }
1974
1975 #[wasm_bindgen(js_name = __beginOpenRtcBroadcastPublication)]
1978 #[allow(clippy::too_many_arguments)]
1979 pub fn begin_openrtc_broadcast_publication(
1980 &self,
1981 handle_id: String,
1982 source_slot: String,
1983 publication_id: Option<String>,
1984 kind: String,
1985 codec: String,
1986 clock_rate: u32,
1987 coded_width: Option<u32>,
1988 coded_height: Option<u32>,
1989 channels: Option<u16>,
1990 ) -> Result<JsValue, JsValue> {
1991 let publication_id = match publication_id {
1992 Some(value) if !value.is_empty() => {
1993 value
1994 .parse()
1995 .map_err(|error: crate::media::MediaProtocolError| {
1996 JsValue::from_str(&error.to_string())
1997 })?
1998 }
1999 _ => {
2000 let mut bytes = [0_u8; 16];
2001 getrandom::getrandom(&mut bytes)
2002 .map_err(|_| JsValue::from_str("media publication entropy failed"))?;
2003 crate::media::PublicationId::from_bytes(bytes)
2004 }
2005 };
2006 let kind: crate::media::MediaKind =
2007 kind.parse()
2008 .map_err(|error: crate::media::MediaProtocolError| {
2009 JsValue::from_str(&error.to_string())
2010 })?;
2011 if !matches!(
2012 kind,
2013 crate::media::MediaKind::Audio | crate::media::MediaKind::Video
2014 ) {
2015 return Err(JsValue::from_str(
2016 "browser broadcast publications must be audio or video",
2017 ));
2018 }
2019 let codec: crate::media::MediaCodec =
2020 codec
2021 .parse()
2022 .map_err(|error: crate::media::MediaProtocolError| {
2023 JsValue::from_str(&error.to_string())
2024 })?;
2025 let publication = self
2026 .broadcast_sessions
2027 .borrow()
2028 .get(&handle_id)
2029 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2030 .begin_publication(
2031 &source_slot,
2032 crate::media::MediaPublicationConfig {
2033 publication_id,
2034 media_generation: 0,
2035 kind,
2036 codec,
2037 clock_rate,
2038 coded_width,
2039 coded_height,
2040 channels,
2041 },
2042 )
2043 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2044 serde::Serialize::serialize(
2045 &serde_json::json!({
2046 "publicationId": publication.publication_id.to_string(),
2047 "mediaGeneration": publication.media_generation,
2048 "control": Vec::<u8>::new(),
2049 }),
2050 &serde_wasm_bindgen::Serializer::json_compatible(),
2051 )
2052 .map_err(|error| JsValue::from_str(&error.to_string()))
2053 }
2054
2055 #[wasm_bindgen(js_name = __publishOpenRtcBroadcastSample)]
2058 #[allow(clippy::too_many_arguments)]
2059 pub fn publish_openrtc_broadcast_sample(
2060 &self,
2061 handle_id: String,
2062 publication_id: String,
2063 timestamp_us: f64,
2064 duration_us: u32,
2065 keyframe: bool,
2066 discardable: bool,
2067 payload: Vec<u8>,
2068 ) -> Result<(), JsValue> {
2069 if !timestamp_us.is_finite()
2070 || timestamp_us < 0.0
2071 || timestamp_us.fract() != 0.0
2072 || timestamp_us > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
2073 {
2074 return Err(JsValue::from_str(
2075 "media timestamp must be a non-negative JavaScript-safe integer",
2076 ));
2077 }
2078 let publication_id =
2079 publication_id
2080 .parse()
2081 .map_err(|error: crate::media::MediaProtocolError| {
2082 JsValue::from_str(&error.to_string())
2083 })?;
2084 self.broadcast_sessions
2085 .borrow()
2086 .get(&handle_id)
2087 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2088 .publish(
2089 publication_id,
2090 crate::media::EncodedMediaSample {
2091 timestamp_us: timestamp_us as u64,
2092 duration_us,
2093 keyframe,
2094 discardable,
2095 payload,
2096 },
2097 )
2098 .map_err(|error| JsValue::from_str(&error.to_string()))
2099 }
2100
2101 #[wasm_bindgen(js_name = __retireOpenRtcBroadcastPublication)]
2102 pub fn retire_openrtc_broadcast_publication(
2103 &self,
2104 handle_id: String,
2105 publication_id: String,
2106 ) -> Result<(), JsValue> {
2107 let publication_id =
2108 publication_id
2109 .parse()
2110 .map_err(|error: crate::media::MediaProtocolError| {
2111 JsValue::from_str(&error.to_string())
2112 })?;
2113 self.broadcast_sessions
2114 .borrow()
2115 .get(&handle_id)
2116 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2117 .retire_publication(publication_id)
2118 .map_err(|error| JsValue::from_str(&error.to_string()))
2119 }
2120
2121 #[wasm_bindgen(js_name = __pauseOpenRtcBroadcastPublication)]
2122 pub fn pause_openrtc_broadcast_publication(
2123 &self,
2124 handle_id: String,
2125 publication_id: String,
2126 ) -> Result<(), JsValue> {
2127 let publication_id =
2128 publication_id
2129 .parse()
2130 .map_err(|error: crate::media::MediaProtocolError| {
2131 JsValue::from_str(&error.to_string())
2132 })?;
2133 self.broadcast_sessions
2134 .borrow()
2135 .get(&handle_id)
2136 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2137 .pause_publication(publication_id)
2138 .map_err(|error| JsValue::from_str(&error.to_string()))
2139 }
2140
2141 #[wasm_bindgen(js_name = __acceptOpenRtcBroadcastObject)]
2144 pub fn accept_openrtc_broadcast_object(
2145 &self,
2146 handle_id: String,
2147 source_slot: String,
2148 encoded: Vec<u8>,
2149 ) -> Result<JsValue, JsValue> {
2150 let accepted = self
2151 .broadcast_sessions
2152 .borrow()
2153 .get(&handle_id)
2154 .ok_or_else(|| JsValue::from_str("broadcast session is unavailable"))?
2155 .accept_media_for_source(&source_slot, &encoded)
2156 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2157 serde::Serialize::serialize(
2158 &accepted,
2159 &serde_wasm_bindgen::Serializer::json_compatible(),
2160 )
2161 .map_err(|error| JsValue::from_str(&error.to_string()))
2162 }
2163
2164 #[wasm_bindgen(js_name = __beginOpenRtcMediaPublication)]
2168 #[allow(clippy::too_many_arguments)]
2169 pub fn begin_openrtc_media_publication(
2170 &self,
2171 publication_id: Option<String>,
2172 kind: String,
2173 codec: String,
2174 clock_rate: u32,
2175 coded_width: Option<u32>,
2176 coded_height: Option<u32>,
2177 channels: Option<u16>,
2178 ) -> Result<JsValue, JsValue> {
2179 let publication_id = match publication_id {
2180 Some(value) if !value.is_empty() => {
2181 value
2182 .parse()
2183 .map_err(|error: crate::media::MediaProtocolError| {
2184 JsValue::from_str(&error.to_string())
2185 })?
2186 }
2187 _ => {
2188 let mut bytes = [0_u8; 16];
2189 getrandom::getrandom(&mut bytes)
2190 .map_err(|_| JsValue::from_str("media publication entropy failed"))?;
2191 crate::media::PublicationId::from_bytes(bytes)
2192 }
2193 };
2194 let kind: crate::media::MediaKind =
2195 kind.parse()
2196 .map_err(|error: crate::media::MediaProtocolError| {
2197 JsValue::from_str(&error.to_string())
2198 })?;
2199 if !matches!(
2200 kind,
2201 crate::media::MediaKind::Audio | crate::media::MediaKind::Video
2202 ) {
2203 return Err(JsValue::from_str(
2204 "browser media publications must be audio or video",
2205 ));
2206 }
2207 let codec: crate::media::MediaCodec =
2208 codec
2209 .parse()
2210 .map_err(|error: crate::media::MediaProtocolError| {
2211 JsValue::from_str(&error.to_string())
2212 })?;
2213 let publication = crate::media::MediaPublicationConfig {
2214 publication_id,
2215 media_generation: 0,
2216 kind,
2217 codec,
2218 clock_rate,
2219 coded_width,
2220 coded_height,
2221 channels,
2222 };
2223 let (publication, control) = self
2224 .portable_media
2225 .borrow_mut()
2226 .begin_publication(publication)
2227 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2228
2229 let object = js_sys::Object::new();
2230 js_sys::Reflect::set(
2231 &object,
2232 &JsValue::from_str("publicationId"),
2233 &JsValue::from_str(&publication_id.to_string()),
2234 )?;
2235 js_sys::Reflect::set(
2236 &object,
2237 &JsValue::from_str("mediaGeneration"),
2238 &JsValue::from_f64(publication.media_generation as f64),
2239 )?;
2240 js_sys::Reflect::set(
2241 &object,
2242 &JsValue::from_str("control"),
2243 &js_sys::Uint8Array::from(control.as_slice()),
2244 )?;
2245 Ok(object.into())
2246 }
2247
2248 #[wasm_bindgen(js_name = __encodeOpenRtcMediaSample)]
2251 pub fn encode_openrtc_media_sample(
2252 &self,
2253 publication_id: String,
2254 timestamp_us: f64,
2255 duration_us: u32,
2256 keyframe: bool,
2257 discardable: bool,
2258 payload: Vec<u8>,
2259 ) -> Result<Vec<u8>, JsValue> {
2260 fn safe_u64(value: f64, field: &str) -> Result<u64, JsValue> {
2261 if !value.is_finite()
2262 || value < 0.0
2263 || value.fract() != 0.0
2264 || value > crate::media::MAX_JAVASCRIPT_SAFE_INTEGER as f64
2265 {
2266 return Err(JsValue::from_str(&format!(
2267 "media {field} must be a non-negative JavaScript-safe integer"
2268 )));
2269 }
2270 Ok(value as u64)
2271 }
2272 let publication_id: crate::media::PublicationId =
2273 publication_id
2274 .parse()
2275 .map_err(|error: crate::media::MediaProtocolError| {
2276 JsValue::from_str(&error.to_string())
2277 })?;
2278 self.portable_media
2279 .borrow_mut()
2280 .encode_sample(
2281 publication_id,
2282 crate::media::EncodedMediaSample {
2283 timestamp_us: safe_u64(timestamp_us, "timestamp")?,
2284 duration_us,
2285 keyframe,
2286 discardable,
2287 payload,
2288 },
2289 )
2290 .map_err(|error| JsValue::from_str(&error.to_string()))
2291 }
2292
2293 #[wasm_bindgen(js_name = __pauseOpenRtcMediaPublication)]
2294 pub fn pause_openrtc_media_publication(
2295 &self,
2296 publication_id: String,
2297 ) -> Result<(), JsValue> {
2298 let publication_id: crate::media::PublicationId =
2299 publication_id
2300 .parse()
2301 .map_err(|error: crate::media::MediaProtocolError| {
2302 JsValue::from_str(&error.to_string())
2303 })?;
2304 self.portable_media
2305 .borrow_mut()
2306 .pause_publication(publication_id)
2307 .map_err(|error| JsValue::from_str(&error.to_string()))
2308 }
2309
2310 #[wasm_bindgen(js_name = __retireOpenRtcMediaPublication)]
2311 pub fn retire_openrtc_media_publication(
2312 &self,
2313 publication_id: String,
2314 ) -> Result<(), JsValue> {
2315 let publication_id: crate::media::PublicationId =
2316 publication_id
2317 .parse()
2318 .map_err(|error: crate::media::MediaProtocolError| {
2319 JsValue::from_str(&error.to_string())
2320 })?;
2321 self.portable_media
2322 .borrow_mut()
2323 .retire_publication(publication_id);
2324 Ok(())
2325 }
2326
2327 #[wasm_bindgen(js_name = __retireOpenRtcMediaReceiver)]
2328 pub fn retire_openrtc_media_receiver(
2329 &self,
2330 publication_id: String,
2331 media_generation: u32,
2332 ) -> Result<(), JsValue> {
2333 let publication_id: crate::media::PublicationId =
2334 publication_id
2335 .parse()
2336 .map_err(|error: crate::media::MediaProtocolError| {
2337 JsValue::from_str(&error.to_string())
2338 })?;
2339 self.portable_media
2340 .borrow_mut()
2341 .retire_receiver(publication_id, media_generation);
2342 Ok(())
2343 }
2344
2345 #[wasm_bindgen(js_name = __decodeOpenRtcMediaChunk)]
2349 pub fn decode_openrtc_media_chunk(&self, encoded: Vec<u8>) -> Result<JsValue, JsValue> {
2350 let chunk = self
2351 .portable_media
2352 .borrow_mut()
2353 .decode_chunk(&encoded)
2354 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2355 crate::media::ensure_javascript_safe_integer(chunk.sequence, "sequence")
2356 .and_then(|_| {
2357 crate::media::ensure_javascript_safe_integer(chunk.timestamp_us, "timestamp")
2358 })
2359 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2360
2361 let object = js_sys::Object::new();
2362 let set = |name: &str, value: JsValue| -> Result<(), JsValue> {
2363 js_sys::Reflect::set(&object, &JsValue::from_str(name), &value).map(|_| ())
2364 };
2365 set(
2366 "publicationId",
2367 JsValue::from_str(&chunk.publication_id.to_string()),
2368 )?;
2369 set(
2370 "mediaGeneration",
2371 JsValue::from_f64(chunk.media_generation as f64),
2372 )?;
2373 set("sequence", JsValue::from_f64(chunk.sequence as f64))?;
2374 set("timestampUs", JsValue::from_f64(chunk.timestamp_us as f64))?;
2375 set("durationUs", JsValue::from_f64(chunk.duration_us as f64))?;
2376 set(
2377 "kind",
2378 JsValue::from_str(match chunk.kind {
2379 crate::media::MediaKind::Audio => "audio",
2380 crate::media::MediaKind::Video => "video",
2381 crate::media::MediaKind::Screen => "screen",
2382 crate::media::MediaKind::Data => "data",
2383 }),
2384 )?;
2385 set(
2386 "codec",
2387 JsValue::from_str(match chunk.codec {
2388 crate::media::MediaCodec::Opus => "opus",
2389 crate::media::MediaCodec::H264 => "h264",
2390 crate::media::MediaCodec::Vp8 => "vp8",
2391 crate::media::MediaCodec::Vp9 => "vp9",
2392 crate::media::MediaCodec::Av1 => "av1",
2393 crate::media::MediaCodec::Pcm => "pcm",
2394 crate::media::MediaCodec::Opaque => "opaque",
2395 }),
2396 )?;
2397 set("keyframe", JsValue::from_bool(chunk.keyframe))?;
2398 set("discardable", JsValue::from_bool(chunk.discardable))?;
2399 set(
2400 "payload",
2401 js_sys::Uint8Array::from(chunk.payload.as_slice()).into(),
2402 )?;
2403 Ok(object.into())
2404 }
2405
2406 #[wasm_bindgen(js_name = __decodeOpenRtcMediaControl)]
2407 pub fn decode_openrtc_media_control(&self, encoded: Vec<u8>) -> Result<JsValue, JsValue> {
2408 let control = self
2409 .portable_media
2410 .borrow_mut()
2411 .decode_control(&encoded)
2412 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2413 let portable = match control {
2418 crate::media::MediaControlFrame::Publish {
2419 publication_id,
2420 media_generation,
2421 kind,
2422 codec,
2423 clock_rate,
2424 coded_width,
2425 coded_height,
2426 channels,
2427 } => serde_json::json!({
2428 "type": "publish",
2429 "publicationId": publication_id.to_string(),
2430 "mediaGeneration": media_generation,
2431 "kind": kind,
2432 "codec": codec,
2433 "clockRate": clock_rate,
2434 "codedWidth": coded_width,
2435 "codedHeight": coded_height,
2436 "channels": channels,
2437 }),
2438 crate::media::MediaControlFrame::SetEnabled {
2439 publication_id,
2440 media_generation,
2441 enabled,
2442 } => serde_json::json!({
2443 "type": "set-enabled",
2444 "publicationId": publication_id.to_string(),
2445 "mediaGeneration": media_generation,
2446 "enabled": enabled,
2447 }),
2448 crate::media::MediaControlFrame::RequestKeyframe {
2449 publication_id,
2450 media_generation,
2451 } => serde_json::json!({
2452 "type": "request-keyframe",
2453 "publicationId": publication_id.to_string(),
2454 "mediaGeneration": media_generation,
2455 }),
2456 crate::media::MediaControlFrame::Stop {
2457 publication_id,
2458 media_generation,
2459 reason,
2460 } => serde_json::json!({
2461 "type": "stop",
2462 "publicationId": publication_id.to_string(),
2463 "mediaGeneration": media_generation,
2464 "reason": reason,
2465 }),
2466 };
2467 serde::Serialize::serialize(
2468 &portable,
2469 &serde_wasm_bindgen::Serializer::json_compatible(),
2470 )
2471 .map_err(|error| JsValue::from_str(&error.to_string()))
2472 }
2473
2474 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2478 #[wasm_bindgen(js_name = __setIrohCarrierActionHandler)]
2479 pub fn set_iroh_carrier_action_handler(&self, handler: Option<js_sys::Function>) {
2480 *self.wasm_carrier_action_handler.borrow_mut() = handler;
2481 }
2482
2483 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2487 #[wasm_bindgen(js_name = __getIrohCarrierReadiness)]
2488 pub async fn get_iroh_carrier_readiness(&self) -> Result<JsValue, JsValue> {
2489 let readiness = serde_json::json!({
2490 "webrtcConfigured": self.inner.is_webrtc_carrier_configured().await,
2491 "moqConfigured": self.inner.is_moq_carrier_configured().await,
2492 "webrtcAvailable": self.inner.is_webrtc_carrier_enabled().await,
2493 "moqAvailable": self.inner.is_moq_carrier_enabled().await,
2494 "actionHandlerInstalled": self.wasm_carrier_action_handler.borrow().is_some(),
2495 });
2496 serde::Serialize::serialize(
2497 &readiness,
2498 &serde_wasm_bindgen::Serializer::json_compatible(),
2499 )
2500 .map_err(|error| JsValue::from_str(&error.to_string()))
2501 }
2502
2503 #[cfg(feature = "transport-webrtc")]
2504 #[wasm_bindgen(js_name = __configureWebRtcCarrier)]
2505 pub async fn configure_iroh_webrtc_carrier(
2506 &self,
2507 enabled: bool,
2508 privacy_mode: bool,
2509 ) -> Result<(), JsValue> {
2510 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2511 self.inner.set_webrtc_carrier(enabled, privacy_mode).await;
2512 Ok(())
2513 }
2514
2515 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2518 #[wasm_bindgen(js_name = __configureIrohRoutePolicy)]
2519 pub async fn configure_iroh_route_policy(
2520 &self,
2521 relay_only: bool,
2522 optimize_for: Option<String>,
2523 route_priority: JsValue,
2524 ) -> Result<(), JsValue> {
2525 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2526 let optimize_for = match optimize_for.as_deref().unwrap_or("balanced") {
2527 "balanced" => crate::route_policy::RouteOptimization::Balanced,
2528 "lowest-latency" => crate::route_policy::RouteOptimization::LowestLatency,
2529 _ => {
2530 return Err(JsValue::from_str(
2531 "transport optimization must be balanced or lowest-latency",
2532 ))
2533 }
2534 };
2535 let route_priority = if route_priority.is_null() || route_priority.is_undefined() {
2536 Vec::new()
2537 } else {
2538 serde_wasm_bindgen::from_value(route_priority)
2539 .map_err(|error| JsValue::from_str(&error.to_string()))?
2540 };
2541 self.inner
2542 .configure_wasm_route_policy(relay_only, optimize_for, route_priority)
2543 .await;
2544 Ok(())
2545 }
2546
2547 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2551 #[wasm_bindgen(js_name = __applyIrohCarrierPolicy)]
2552 pub async fn apply_iroh_carrier_policy(&self) -> Result<u32, JsValue> {
2553 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2554 let capabilities: Vec<_> = self
2555 .wasm_remote_carrier_capabilities
2556 .borrow()
2557 .iter()
2558 .map(|(connection_id, capabilities)| (connection_id.clone(), *capabilities))
2559 .collect();
2560 let mut reconciled = 0u32;
2561 for (connection_id, capabilities) in capabilities {
2562 let Some(remote_endpoint_id) = self
2563 .inner
2564 .wasm_iroh_carrier_remote_endpoint_id(&connection_id)
2565 .await
2566 else {
2567 continue;
2568 };
2569 if self
2570 .begin_preferred_iroh_carrier_locked(
2571 connection_id,
2572 remote_endpoint_id,
2573 capabilities.webrtc,
2574 capabilities.moq,
2575 )
2576 .await?
2577 {
2578 reconciled = reconciled.saturating_add(1);
2579 }
2580 }
2581 Ok(reconciled)
2582 }
2583
2584 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2591 #[wasm_bindgen(js_name = __reconcileIrohCarriersAfterNetworkChange)]
2592 pub async fn reconcile_iroh_carriers_after_network_change(&self) -> u32 {
2593 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2594 let mut reconciled = 0u32;
2595 let mut recoveries = Vec::new();
2596 #[cfg(feature = "transport-webrtc")]
2597 {
2598 let attempts: Vec<_> = self
2599 .wasm_webrtc_carrier_attempts
2600 .borrow()
2601 .values()
2602 .cloned()
2603 .collect();
2604 for attempt in attempts {
2605 if retire_selected_wasm_webrtc_carrier(
2606 self.inner.clone(),
2607 self.wasm_webrtc_carrier_attempts.clone(),
2608 self.wasm_webrtc_carrier_sessions.clone(),
2609 self.wasm_carrier_action_handler.clone(),
2610 &attempt,
2611 "network-change",
2612 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
2613 )
2614 .await
2615 {
2616 reconciled = reconciled.saturating_add(1);
2617 recoveries.push((
2618 attempt.connection_id.clone(),
2619 attempt.remote_endpoint_id,
2620 attempt.generation.transport_generation.saturating_add(1),
2621 ));
2622 }
2623 }
2624 }
2625 #[cfg(feature = "transport-moq")]
2626 {
2627 let attempts: Vec<_> = self
2628 .wasm_moq_carrier_attempts
2629 .borrow()
2630 .values()
2631 .cloned()
2632 .collect();
2633 for attempt in attempts {
2634 if retire_selected_wasm_moq_carrier(
2635 self.inner.clone(),
2636 self.wasm_moq_carrier_attempts.clone(),
2637 self.wasm_moq_carrier_sessions.clone(),
2638 self.wasm_carrier_action_handler.clone(),
2639 &attempt,
2640 "network-change",
2641 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
2642 )
2643 .await
2644 {
2645 reconciled = reconciled.saturating_add(1);
2646 recoveries.push((
2647 attempt.connection_id.clone(),
2648 attempt.remote_endpoint_id,
2649 attempt.generation.transport_generation.saturating_add(1),
2650 ));
2651 }
2652 }
2653 }
2654 for (connection_id, remote_endpoint_id, transport_generation) in recoveries {
2655 let recovered = self
2656 .inner
2657 .recover_retired_wasm_carrier_generation(
2658 &connection_id,
2659 remote_endpoint_id,
2660 transport_generation,
2661 )
2662 .await;
2663 if !recovered {
2664 web_sys::console::warn_1(&JsValue::from_str(&format!(
2665 "[OpenRTC][Iroh carrier] base recovery remained pending connection_id={connection_id} transport_generation={transport_generation}"
2666 )));
2667 }
2668 }
2669 if reconciled > 0 {
2670 self.inner.wake_browser_auto_connect();
2677 }
2678 reconciled
2679 }
2680
2681 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2685 #[wasm_bindgen(js_name = __forgetIrohCarrierPeer)]
2686 pub async fn forget_iroh_carrier_peer(&self, connection_id: String) {
2687 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2688 self.forget_iroh_carrier_peer_locked(&connection_id);
2689 }
2690
2691 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2692 fn forget_iroh_carrier_peer_locked(&self, connection_id: &str) {
2693 self.wasm_remote_carrier_capabilities
2694 .borrow_mut()
2695 .remove(connection_id);
2696 self.inner
2697 .forget_iroh_carrier_peer_policy_epoch(connection_id);
2698 }
2699
2700 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2705 #[wasm_bindgen(js_name = __retireIrohCarrierPeer)]
2706 pub async fn retire_iroh_carrier_peer(
2707 &self,
2708 connection_id: String,
2709 terminal_reason: Option<String>,
2710 ) {
2711 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2712 #[cfg(feature = "transport-webrtc")]
2713 self.retire_iroh_webrtc_carrier_locked(
2714 connection_id.clone(),
2715 terminal_reason.as_deref(),
2716 )
2717 .await;
2718 #[cfg(feature = "transport-moq")]
2719 self.retire_iroh_moq_carrier_locked(connection_id.clone(), terminal_reason.as_deref())
2720 .await;
2721 self.forget_iroh_carrier_peer_locked(&connection_id);
2722 }
2723
2724 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2725 fn has_matching_in_flight_wasm_carrier_attempt(
2726 &self,
2727 connection_id: &str,
2728 capabilities: WasmRemoteCarrierCapabilities,
2729 ) -> bool {
2730 #[cfg(feature = "transport-webrtc")]
2731 if capabilities.webrtc
2732 && self
2733 .wasm_webrtc_carrier_attempts
2734 .borrow()
2735 .get(connection_id)
2736 .is_some_and(|attempt| {
2737 attempt.role == "responder" && !attempt.completion_started
2738 })
2739 {
2740 return true;
2741 }
2742 #[cfg(feature = "transport-moq")]
2743 if capabilities.moq
2744 && self
2745 .wasm_moq_carrier_attempts
2746 .borrow()
2747 .get(connection_id)
2748 .is_some_and(|attempt| attempt.role == "responder")
2749 {
2750 return true;
2751 }
2752 false
2753 }
2754
2755 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2756 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2759 #[wasm_bindgen(js_name = __beginPreferredIrohCarrier)]
2760 pub async fn begin_preferred_iroh_carrier(
2761 &self,
2762 connection_id: String,
2763 remote_endpoint_id: String,
2764 remote_supports_webrtc: bool,
2765 remote_supports_moq: bool,
2766 ) -> Result<bool, JsValue> {
2767 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2768 self.begin_preferred_iroh_carrier_locked(
2769 connection_id,
2770 remote_endpoint_id,
2771 remote_supports_webrtc,
2772 remote_supports_moq,
2773 )
2774 .await
2775 }
2776
2777 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2778 async fn begin_preferred_iroh_carrier_locked(
2779 &self,
2780 connection_id: String,
2781 remote_endpoint_id: String,
2782 remote_supports_webrtc: bool,
2783 remote_supports_moq: bool,
2784 ) -> Result<bool, JsValue> {
2785 let capabilities = WasmRemoteCarrierCapabilities {
2786 webrtc: remote_supports_webrtc,
2787 moq: remote_supports_moq,
2788 };
2789 let previous_capabilities = self
2790 .wasm_remote_carrier_capabilities
2791 .borrow()
2792 .get(&connection_id)
2793 .copied();
2794 if previous_capabilities != Some(capabilities) {
2795 let matching_first_attempt = previous_capabilities.is_none()
2800 && self
2801 .has_matching_in_flight_wasm_carrier_attempt(&connection_id, capabilities);
2802 if crate::client::should_bump_remote_carrier_peer_policy_epoch(
2803 previous_capabilities.is_some(),
2804 true,
2805 matching_first_attempt,
2806 ) {
2807 self.inner
2808 .bump_wasm_iroh_carrier_peer_policy_epoch(&connection_id)
2809 .await;
2810 }
2811 self.wasm_remote_carrier_capabilities
2812 .borrow_mut()
2813 .insert(connection_id.clone(), capabilities);
2814 }
2815 if self
2816 .inner
2817 .reconcile_wasm_iroh_carrier_policy(
2818 &connection_id,
2819 remote_supports_webrtc,
2820 remote_supports_moq,
2821 )
2822 .await
2823 .map_err(|error| JsValue::from_str(&error.to_string()))?
2824 {
2825 return Ok(true);
2826 }
2827 let ranked = self
2828 .inner
2829 .ranked_wasm_iroh_carriers(remote_supports_webrtc, remote_supports_moq)
2830 .await;
2831 match ranked.first() {
2832 #[cfg(feature = "transport-webrtc")]
2833 Some(crate::route_policy::KnownRoute::WebRtc) => {
2834 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
2835 .await
2836 }
2837 #[cfg(feature = "transport-moq")]
2838 Some(crate::route_policy::KnownRoute::Moq) => {
2839 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2840 .await
2841 }
2842 _ => Ok(false),
2843 }
2844 }
2845
2846 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
2849 #[wasm_bindgen(js_name = __advancePreferredIrohCarrier)]
2850 pub async fn advance_preferred_iroh_carrier(
2851 &self,
2852 connection_id: String,
2853 remote_endpoint_id: String,
2854 failed_route: String,
2855 ) -> Result<bool, JsValue> {
2856 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2857 let Some(capabilities) = self
2858 .wasm_remote_carrier_capabilities
2859 .borrow()
2860 .get(&connection_id)
2861 .copied()
2862 else {
2863 return Ok(false);
2864 };
2865 let failed = crate::route_policy::normalize_route(&failed_route)
2866 .ok_or_else(|| JsValue::from_str("unknown failed carrier route"))?;
2867 let ranked = self
2868 .inner
2869 .ranked_wasm_iroh_carriers(capabilities.webrtc, capabilities.moq)
2870 .await;
2871 let Some(next_index) = ranked
2872 .iter()
2873 .position(|route| *route == failed)
2874 .map(|index| index + 1)
2875 .filter(|index| *index < ranked.len())
2876 else {
2877 return Ok(false);
2878 };
2879 let next = ranked[next_index];
2880 match next {
2881 #[cfg(feature = "transport-webrtc")]
2882 crate::route_policy::KnownRoute::WebRtc => {
2883 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
2884 .await
2885 }
2886 #[cfg(feature = "transport-moq")]
2887 crate::route_policy::KnownRoute::Moq => {
2888 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2889 .await
2890 }
2891 _ => Ok(false),
2892 }
2893 }
2894
2895 #[cfg(feature = "transport-moq")]
2896 #[wasm_bindgen(js_name = __configureMoqCarrier)]
2897 pub async fn configure_iroh_moq_carrier(
2898 &self,
2899 enabled: bool,
2900 relay_url: Option<String>,
2901 ) -> Result<(), JsValue> {
2902 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2903 let relay_url = relay_url.map(|value| value.trim().to_string());
2904 if enabled && relay_url.as_deref().unwrap_or_default().is_empty() {
2905 return Err(JsValue::from_str(
2906 "MoQ Iroh carrier requires an explicit relay URL",
2907 ));
2908 }
2909 self.inner.set_moq_carrier(enabled, relay_url).await;
2910 Ok(())
2911 }
2912
2913 #[cfg(feature = "transport-moq")]
2916 #[wasm_bindgen(js_name = __beginMoqCarrier)]
2917 pub async fn begin_iroh_moq_carrier(
2918 &self,
2919 connection_id: String,
2920 remote_endpoint_id: String,
2921 ) -> Result<bool, JsValue> {
2922 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
2923 self.begin_iroh_moq_carrier_attempt(connection_id, remote_endpoint_id, 0)
2924 .await
2925 }
2926
2927 #[cfg(feature = "transport-moq")]
2928 async fn begin_iroh_moq_carrier_attempt(
2929 &self,
2930 connection_id: String,
2931 remote_endpoint_id: String,
2932 retry_count: u8,
2933 ) -> Result<bool, JsValue> {
2934 if !self.inner.is_moq_carrier_enabled().await {
2935 return Ok(false);
2936 }
2937 let local_endpoint_id = self
2938 .inner
2939 .current_node_id()
2940 .await
2941 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
2942 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
2943 || !crate::iroh_connection_policy::custom_carrier_base_allows(
2944 self.inner.iroh_path_kind(&remote_endpoint_id).await,
2945 crate::client::IrohPathKind::Moq,
2946 false,
2947 )
2948 {
2949 return Ok(false);
2950 }
2951 let endpoint_id = remote_endpoint_id
2952 .parse::<iroh::EndpointId>()
2953 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2954 self.inner
2955 .get_connection(endpoint_id)
2956 .await
2957 .ok_or_else(|| JsValue::from_str("MoQ carrier base is unavailable"))?;
2958 let generation = self
2959 .inner
2960 .current_wasm_peer_data_generation(&connection_id, None)
2961 .await
2962 .ok_or_else(|| JsValue::from_str("MoQ carrier generation is unavailable"))?;
2963 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
2964 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14,
2965 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
2966 transport_stable_id: generation.transport_stable_id,
2967 transport_generation: generation.transport_generation,
2968 route_generation: generation.route_generation,
2969 },
2970 retry_count.saturating_add(1),
2971 )
2972 .map_err(|error| JsValue::from_str(&error.to_string()))?;
2973 let kind = crate::client::IrohPathKind::Moq;
2974 if !matches!(
2975 self.inner
2976 .reserve_wasm_carrier_upgrade(
2977 &connection_id,
2978 kind,
2979 &bootstrap.upgrade_id,
2980 generation,
2981 )
2982 .await,
2983 crate::client::WasmCarrierUpgradeReservation::Reserved
2984 ) {
2985 let pending = self
2986 .wasm_moq_carrier_attempts
2987 .borrow_mut()
2988 .get_mut(&connection_id)
2989 .filter(|attempt| {
2990 attempt.role == "initiator"
2991 && attempt.generation == generation
2992 && attempt.prepared
2993 && !attempt.retry_sent
2994 })
2995 .map(|attempt| {
2996 attempt.retry_sent = true;
2997 attempt.clone()
2998 });
2999 if let Some(pending) = pending {
3000 emit_wasm_carrier_action(
3006 &self.wasm_carrier_action_handler,
3007 serde_json::json!({
3008 "type": "send-control",
3009 "connectionId": connection_id,
3010 "remoteEndpointId": pending.remote_endpoint_id.to_string(),
3011 "envelope": pending.bootstrap,
3012 }),
3013 );
3014 return Ok(true);
3015 }
3016 return Ok(false);
3017 }
3018 let previous = self.wasm_moq_carrier_attempts.borrow_mut().insert(
3019 connection_id.clone(),
3020 WasmMoqCarrierAttempt {
3021 connection_id: connection_id.clone(),
3022 remote_endpoint_id: endpoint_id,
3023 bootstrap: bootstrap.clone(),
3024 generation,
3025 role: "initiator",
3026 prepared: false,
3027 retry_sent: false,
3028 retry_count,
3029 inbound_authorization_expires_at_ms: None,
3030 },
3031 );
3032 if let Some(previous) = previous {
3033 self.wasm_moq_carrier_sessions
3034 .borrow_mut()
3035 .remove(&previous.bootstrap.upgrade_id);
3036 }
3037 let (publish_namespace, subscribe_namespace, track_name) = wasm_moq_carrier_namespaces(
3038 &local_endpoint_id,
3039 &remote_endpoint_id,
3040 &bootstrap.carrier_session_id,
3041 );
3042 emit_wasm_carrier_action(
3043 &self.wasm_carrier_action_handler,
3044 serde_json::json!({
3045 "type": "prepare-moq",
3046 "connectionId": connection_id,
3047 "remoteEndpointId": remote_endpoint_id,
3048 "role": "initiator",
3049 "upgradeId": bootstrap.upgrade_id,
3050 "carrierSessionId": bootstrap.carrier_session_id,
3051 "transportGeneration": generation.transport_generation.saturating_add(1),
3052 "publishNamespace": publish_namespace,
3053 "subscribeNamespace": subscribe_namespace,
3054 "trackName": track_name,
3055 }),
3056 );
3057 self.schedule_wasm_moq_carrier_watchdog(bootstrap);
3058 Ok(true)
3059 }
3060
3061 #[cfg(feature = "transport-moq")]
3062 #[wasm_bindgen(js_name = __moqCarrierPrepared)]
3063 pub async fn iroh_moq_carrier_prepared(
3064 &self,
3065 connection_id: String,
3066 upgrade_id: String,
3067 ) -> Result<(), JsValue> {
3068 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3069 let attempt = {
3070 let mut attempts = self.wasm_moq_carrier_attempts.borrow_mut();
3071 let attempt = attempts
3072 .get_mut(&connection_id)
3073 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3074 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
3075 attempt.prepared = true;
3076 attempt.clone()
3077 };
3078 if !self
3079 .inner
3080 .wasm_carrier_upgrade_is_current(
3081 &connection_id,
3082 crate::client::IrohPathKind::Moq,
3083 &upgrade_id,
3084 attempt.generation,
3085 )
3086 .await
3087 {
3088 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
3089 }
3090 if attempt.role == "initiator" {
3091 emit_wasm_carrier_action(
3092 &self.wasm_carrier_action_handler,
3093 serde_json::json!({
3094 "type": "send-control",
3095 "connectionId": connection_id,
3096 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3097 "envelope": attempt.bootstrap,
3098 }),
3099 );
3100 } else {
3101 emit_wasm_carrier_action(
3107 &self.wasm_carrier_action_handler,
3108 serde_json::json!({
3109 "type": "activate-moq",
3110 "connectionId": attempt.connection_id,
3111 "upgradeId": upgrade_id,
3112 }),
3113 );
3114 }
3115 Ok(())
3116 }
3117
3118 #[cfg(feature = "transport-moq")]
3119 #[wasm_bindgen(js_name = __moqCarrierFailed)]
3120 pub async fn iroh_moq_carrier_failed(
3121 &self,
3122 connection_id: String,
3123 upgrade_id: String,
3124 failure_code: String,
3125 ) {
3126 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3127 let attempt = self
3128 .wasm_moq_carrier_attempts
3129 .borrow()
3130 .get(&connection_id)
3131 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3132 .cloned();
3133 if let Some(attempt) = attempt {
3134 let failure_code = match failure_code.as_str() {
3135 "relay-failed" => "relay-failed",
3136 "draft14-datagram-unavailable" => "draft14-datagram-unavailable",
3137 "carrier-backpressure" => "carrier-backpressure",
3138 _ => "browser-adapter-failed",
3139 };
3140 if retire_selected_wasm_moq_carrier(
3141 self.inner.clone(),
3142 self.wasm_moq_carrier_attempts.clone(),
3143 self.wasm_moq_carrier_sessions.clone(),
3144 self.wasm_carrier_action_handler.clone(),
3145 &attempt,
3146 failure_code,
3147 crate::lifecycle_reason::REASON_IROH_CARRIER_FAILED,
3148 )
3149 .await
3150 {
3151 return;
3152 }
3153 self.fail_wasm_moq_carrier_attempt(attempt, failure_code, true)
3154 .await;
3155 }
3156 }
3157
3158 #[cfg(feature = "transport-moq")]
3159 #[wasm_bindgen(js_name = __retireMoqCarrier)]
3160 pub async fn retire_iroh_moq_carrier(
3161 &self,
3162 connection_id: String,
3163 terminal_reason: Option<String>,
3164 ) {
3165 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3166 self.retire_iroh_moq_carrier_locked(connection_id, terminal_reason.as_deref())
3167 .await;
3168 }
3169
3170 #[cfg(feature = "transport-moq")]
3171 async fn retire_iroh_moq_carrier_locked(
3172 &self,
3173 connection_id: String,
3174 terminal_reason: Option<&str>,
3175 ) {
3176 let attempt = self
3177 .wasm_moq_carrier_attempts
3178 .borrow_mut()
3179 .remove(&connection_id);
3180 if let Some(attempt) = attempt {
3181 self.inner
3182 .retire_wasm_carrier_upgrade(
3183 &connection_id,
3184 crate::client::IrohPathKind::Moq,
3185 &attempt.bootstrap.upgrade_id,
3186 attempt.generation,
3187 )
3188 .await;
3189 let carrier = self
3190 .wasm_moq_carrier_sessions
3191 .borrow_mut()
3192 .remove(&attempt.bootstrap.upgrade_id);
3193 if let (Some(reason), Some(session)) = (terminal_reason, carrier.as_ref()) {
3194 let _ = session.send_terminal(reason).await;
3195 }
3196 emit_wasm_carrier_action(
3197 &self.wasm_carrier_action_handler,
3198 serde_json::json!({
3199 "type": "retire-moq",
3200 "connectionId": connection_id,
3201 "upgradeId": attempt.bootstrap.upgrade_id,
3202 "failureCode": "logical-connection-retired",
3203 }),
3204 );
3205 }
3206 }
3207
3208 #[cfg(feature = "transport-webrtc")]
3211 #[wasm_bindgen(js_name = __beginWebRtcCarrier)]
3212 pub async fn begin_iroh_webrtc_carrier(
3213 &self,
3214 connection_id: String,
3215 remote_endpoint_id: String,
3216 ) -> Result<bool, JsValue> {
3217 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3218 self.begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 0)
3219 .await
3220 }
3221
3222 #[cfg(feature = "transport-webrtc")]
3223 async fn begin_iroh_webrtc_carrier_attempt(
3224 &self,
3225 connection_id: String,
3226 remote_endpoint_id: String,
3227 retry_count: u8,
3228 ) -> Result<bool, JsValue> {
3229 if !self.inner.is_webrtc_carrier_enabled().await {
3230 return Ok(false);
3231 }
3232 let local_endpoint_id = self
3233 .inner
3234 .current_node_id()
3235 .await
3236 .ok_or_else(|| JsValue::from_str("local Iroh endpoint id is unavailable"))?;
3237 if local_endpoint_id.as_str() <= remote_endpoint_id.as_str()
3238 || !crate::iroh_connection_policy::custom_carrier_base_allows(
3239 self.inner.iroh_path_kind(&remote_endpoint_id).await,
3240 crate::client::IrohPathKind::WebRtc,
3241 false,
3242 )
3243 {
3244 return Ok(false);
3245 }
3246 let endpoint_id = remote_endpoint_id
3247 .parse::<iroh::EndpointId>()
3248 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3249 self.inner
3250 .get_connection(endpoint_id)
3251 .await
3252 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
3253 let generation = self
3254 .inner
3255 .current_wasm_peer_data_generation(&connection_id, None)
3256 .await
3257 .ok_or_else(|| JsValue::from_str("WebRTC carrier generation is unavailable"))?;
3258 let bootstrap = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::request(
3259 crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc,
3260 crate::iroh_carrier_bootstrap::CarrierGenerationFence {
3261 transport_stable_id: generation.transport_stable_id,
3262 transport_generation: generation.transport_generation,
3263 route_generation: generation.route_generation,
3264 },
3265 1,
3266 )
3267 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3268 let kind = crate::client::IrohPathKind::WebRtc;
3269 if !matches!(
3270 self.inner
3271 .reserve_wasm_carrier_upgrade(
3272 &connection_id,
3273 kind,
3274 &bootstrap.upgrade_id,
3275 generation,
3276 )
3277 .await,
3278 crate::client::WasmCarrierUpgradeReservation::Reserved
3279 ) {
3280 let pending = self
3281 .wasm_webrtc_carrier_attempts
3282 .borrow_mut()
3283 .get_mut(&connection_id)
3284 .filter(|attempt| {
3285 attempt.role == "initiator"
3286 && attempt.generation == generation
3287 && attempt.prepared
3288 && !attempt.retry_sent
3289 && !attempt.completion_started
3290 })
3291 .map(|attempt| {
3292 attempt.retry_sent = true;
3293 attempt.clone()
3294 });
3295 if let Some(pending) = pending {
3296 emit_wasm_carrier_action(
3300 &self.wasm_carrier_action_handler,
3301 serde_json::json!({
3302 "type": "send-control",
3303 "connectionId": connection_id,
3304 "remoteEndpointId": pending.remote_endpoint_id.to_string(),
3305 "envelope": pending.bootstrap,
3306 }),
3307 );
3308 return Ok(true);
3309 }
3310 return Ok(false);
3311 }
3312 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
3313 connection_id.clone(),
3314 WasmWebRtcCarrierAttempt {
3315 connection_id: connection_id.clone(),
3316 remote_endpoint_id: endpoint_id,
3317 bootstrap: bootstrap.clone(),
3318 generation,
3319 role: "initiator",
3320 prepared: false,
3321 retry_sent: false,
3322 offer_started: false,
3323 remote_ready: false,
3324 completion_started: false,
3325 retry_count,
3326 inbound_authorization_expires_at_ms: None,
3327 },
3328 );
3329 if let Some(previous) = previous {
3330 self.wasm_webrtc_carrier_sessions
3331 .borrow_mut()
3332 .remove(&previous.bootstrap.upgrade_id);
3333 }
3334 emit_wasm_carrier_action(
3335 &self.wasm_carrier_action_handler,
3336 serde_json::json!({
3337 "type": "prepare-webrtc",
3338 "connectionId": connection_id,
3339 "remoteEndpointId": remote_endpoint_id,
3340 "role": "initiator",
3341 "upgradeId": bootstrap.upgrade_id,
3342 "carrierSessionId": bootstrap.carrier_session_id,
3343 "transportGeneration": generation.transport_generation.saturating_add(1),
3344 }),
3345 );
3346 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
3347 Ok(true)
3348 }
3349
3350 #[cfg(feature = "transport-webrtc")]
3353 #[wasm_bindgen(js_name = __webRtcCarrierPrepared)]
3354 pub async fn iroh_webrtc_carrier_prepared(
3355 &self,
3356 connection_id: String,
3357 upgrade_id: String,
3358 ) -> Result<(), JsValue> {
3359 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3360 let attempt = {
3361 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
3362 let attempt = attempts
3363 .get_mut(&connection_id)
3364 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3365 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
3366 attempt.prepared = true;
3367 attempt.clone()
3368 };
3369 let kind = crate::client::IrohPathKind::WebRtc;
3370 if !self
3371 .inner
3372 .wasm_carrier_upgrade_is_current(
3373 &connection_id,
3374 kind,
3375 &upgrade_id,
3376 attempt.generation,
3377 )
3378 .await
3379 {
3380 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
3381 }
3382 if attempt.role == "initiator" {
3383 emit_wasm_carrier_action(
3384 &self.wasm_carrier_action_handler,
3385 serde_json::json!({
3386 "type": "send-control",
3387 "connectionId": connection_id,
3388 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3389 "envelope": attempt.bootstrap,
3390 }),
3391 );
3392 } else {
3393 emit_wasm_carrier_action(
3398 &self.wasm_carrier_action_handler,
3399 serde_json::json!({
3400 "type": "send-control",
3401 "connectionId": attempt.connection_id,
3402 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
3403 "envelope": {
3404 "type": "#pluto-signal",
3405 "content": {
3406 "transport": "webrtc",
3407 "type": "renegotiate",
3408 "negotiationId": upgrade_id,
3409 }
3410 },
3411 }),
3412 );
3413 }
3414 Ok(())
3415 }
3416
3417 #[cfg(feature = "transport-webrtc")]
3418 #[wasm_bindgen(js_name = __webRtcCarrierFailed)]
3419 pub async fn iroh_webrtc_carrier_failed(
3420 &self,
3421 connection_id: String,
3422 upgrade_id: String,
3423 failure_code: String,
3424 ) {
3425 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3426 let attempt = self
3427 .wasm_webrtc_carrier_attempts
3428 .borrow()
3429 .get(&connection_id)
3430 .filter(|attempt| attempt.bootstrap.upgrade_id == upgrade_id)
3431 .cloned();
3432 if let Some(attempt) = attempt {
3433 let failure_code = match failure_code.as_str() {
3434 "data-channel-failed" => "data-channel-failed",
3435 "ice-failed" => "ice-failed",
3436 "signaling-failed" => "signaling-failed",
3437 "carrier-backpressure" => "carrier-backpressure",
3438 "carrier-base-generation-stale" => "carrier-base-generation-stale",
3439 _ => "browser-adapter-failed",
3440 };
3441 if retire_selected_wasm_webrtc_carrier(
3442 self.inner.clone(),
3443 self.wasm_webrtc_carrier_attempts.clone(),
3444 self.wasm_webrtc_carrier_sessions.clone(),
3445 self.wasm_carrier_action_handler.clone(),
3446 &attempt,
3447 failure_code,
3448 crate::lifecycle_reason::REASON_IROH_CARRIER_FAILED,
3449 )
3450 .await
3451 {
3452 return;
3453 }
3454 let should_retry = attempt.role == "initiator"
3455 && attempt.retry_count == 0
3456 && matches!(
3457 failure_code,
3458 "data-channel-failed"
3459 | "ice-failed"
3460 | "signaling-failed"
3461 | "carrier-base-generation-stale"
3462 );
3463 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
3464 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, true)
3465 .await;
3466 if should_retry {
3467 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
3472 let _ = self
3473 .begin_iroh_webrtc_carrier_attempt(connection_id, remote_endpoint_id, 1)
3474 .await;
3475 }
3476 }
3477 }
3478
3479 #[cfg(feature = "transport-webrtc")]
3480 #[wasm_bindgen(js_name = __retireWebRtcCarrier)]
3481 pub async fn retire_iroh_webrtc_carrier(
3482 &self,
3483 connection_id: String,
3484 terminal_reason: Option<String>,
3485 ) {
3486 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3487 self.retire_iroh_webrtc_carrier_locked(connection_id, terminal_reason.as_deref())
3488 .await;
3489 }
3490
3491 #[cfg(feature = "transport-webrtc")]
3492 async fn retire_iroh_webrtc_carrier_locked(
3493 &self,
3494 connection_id: String,
3495 terminal_reason: Option<&str>,
3496 ) {
3497 let attempt = self
3498 .wasm_webrtc_carrier_attempts
3499 .borrow_mut()
3500 .remove(&connection_id);
3501 if let Some(attempt) = attempt {
3502 self.inner
3503 .retire_wasm_carrier_upgrade(
3504 &connection_id,
3505 crate::client::IrohPathKind::WebRtc,
3506 &attempt.bootstrap.upgrade_id,
3507 attempt.generation,
3508 )
3509 .await;
3510 let carrier = self
3511 .wasm_webrtc_carrier_sessions
3512 .borrow_mut()
3513 .remove(&attempt.bootstrap.upgrade_id);
3514 if let (Some(reason), Some(session)) = (terminal_reason, carrier.as_ref()) {
3515 let _ = session.send_terminal(reason).await;
3516 }
3517 emit_wasm_carrier_action(
3518 &self.wasm_carrier_action_handler,
3519 serde_json::json!({
3520 "type": "retire-webrtc",
3521 "connectionId": connection_id,
3522 "upgradeId": attempt.bootstrap.upgrade_id,
3523 "failureCode": "logical-connection-retired",
3524 }),
3525 );
3526 }
3527 }
3528
3529 #[cfg(feature = "transport-webrtc")]
3532 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
3533 pub async fn handle_iroh_carrier_control(
3534 &self,
3535 connection_id: String,
3536 remote_endpoint_id: String,
3537 frame: JsValue,
3538 ) -> Result<bool, JsValue> {
3539 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3540 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
3541 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3542 if frame.get("type").and_then(serde_json::Value::as_str) == Some("#pluto-signal")
3543 && frame
3544 .get("content")
3545 .and_then(|content| content.get("transport"))
3546 .and_then(serde_json::Value::as_str)
3547 == Some("webrtc")
3548 {
3549 let negotiation_id = frame
3550 .get("content")
3551 .and_then(|content| content.get("negotiationId"))
3552 .and_then(serde_json::Value::as_str)
3553 .unwrap_or_default();
3554 let current = self
3555 .wasm_webrtc_carrier_attempts
3556 .borrow()
3557 .get(&connection_id)
3558 .is_some_and(|attempt| {
3559 attempt.bootstrap.upgrade_id == negotiation_id
3560 && attempt.remote_endpoint_id.to_string() == remote_endpoint_id
3561 });
3562 if current {
3563 let signal_type = frame
3564 .get("content")
3565 .and_then(|content| content.get("type"))
3566 .and_then(serde_json::Value::as_str);
3567 let start_offer = if signal_type == Some("renegotiate") {
3568 self.wasm_webrtc_carrier_attempts
3569 .borrow_mut()
3570 .get_mut(&connection_id)
3571 .filter(|attempt| {
3572 attempt.bootstrap.upgrade_id == negotiation_id
3573 && attempt.role == "initiator"
3574 && !attempt.offer_started
3575 })
3576 .map(|attempt| {
3577 attempt.offer_started = true;
3578 })
3579 .is_some()
3580 } else {
3581 false
3582 };
3583 if start_offer {
3584 emit_wasm_carrier_action(
3585 &self.wasm_carrier_action_handler,
3586 serde_json::json!({
3587 "type": "start-webrtc",
3588 "connectionId": connection_id,
3589 "upgradeId": negotiation_id,
3590 }),
3591 );
3592 return Ok(true);
3593 }
3594 emit_wasm_carrier_action(
3595 &self.wasm_carrier_action_handler,
3596 serde_json::json!({
3597 "type": "apply-webrtc-signal",
3598 "connectionId": connection_id,
3599 "upgradeId": negotiation_id,
3600 "signal": frame,
3601 }),
3602 );
3603 }
3604 return Ok(true);
3605 }
3606
3607 let Some(bootstrap) =
3608 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
3609 else {
3610 return Ok(false);
3611 };
3612 #[cfg(feature = "transport-moq")]
3613 if bootstrap.carrier == crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
3614 {
3615 return self
3616 .handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
3617 .await;
3618 }
3619 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::WebRtc {
3620 return Ok(false);
3621 }
3622 let endpoint_id = remote_endpoint_id
3623 .parse::<iroh::EndpointId>()
3624 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3625 let kind = crate::client::IrohPathKind::WebRtc;
3626 match bootstrap.action {
3627 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Request => {
3628 let local_endpoint_id = self
3629 .inner
3630 .current_node_id()
3631 .await
3632 .ok_or_else(|| JsValue::from_str("local endpoint id is unavailable"))?;
3633 if local_endpoint_id.as_str() >= remote_endpoint_id.as_str()
3634 || !self.inner.is_webrtc_carrier_enabled().await
3635 {
3636 return Ok(true);
3637 }
3638 if !crate::iroh_connection_policy::custom_carrier_base_allows(
3639 self.inner.iroh_path_kind(&remote_endpoint_id).await,
3640 crate::client::IrohPathKind::WebRtc,
3641 false,
3642 ) {
3643 return Ok(true);
3644 }
3645 let base_connection = self
3646 .inner
3647 .get_connection(endpoint_id)
3648 .await
3649 .ok_or_else(|| JsValue::from_str("WebRTC carrier base is unavailable"))?;
3650 let generation = self
3651 .inner
3652 .current_wasm_peer_data_generation(&connection_id, None)
3653 .await
3654 .ok_or_else(|| {
3655 JsValue::from_str("WebRTC carrier generation is unavailable")
3656 })?;
3657 if crate::transport_generation::for_connection(&base_connection)
3658 != generation.transport_stable_id
3659 {
3660 let failed =
3665 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::failed_from(
3666 &bootstrap,
3667 "carrier-base-generation-stale",
3668 )
3669 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3670 emit_wasm_carrier_action(
3671 &self.wasm_carrier_action_handler,
3672 serde_json::json!({
3673 "type": "send-control",
3674 "connectionId": connection_id,
3675 "remoteEndpointId": remote_endpoint_id,
3676 "envelope": failed,
3677 }),
3678 );
3679 return Ok(true);
3680 }
3681 if !matches!(
3682 self.inner
3683 .reserve_wasm_carrier_upgrade(
3684 &connection_id,
3685 kind,
3686 &bootstrap.upgrade_id,
3687 generation,
3688 )
3689 .await,
3690 crate::client::WasmCarrierUpgradeReservation::Reserved
3691 ) {
3692 return Ok(true);
3693 }
3694 let node = self
3695 .inner
3696 .iroh_node
3697 .read()
3698 .await
3699 .as_ref()
3700 .cloned()
3701 .ok_or_else(|| JsValue::from_str("Iroh node is unavailable"))?;
3702 let authorization_expiry = node
3703 .authorize_pending_inbound_replacement(
3704 endpoint_id,
3705 crate::iroh_carrier_kind::EXPERIMENTAL_WEBRTC_TRANSPORT_ID,
3706 std::time::Duration::from_secs(45),
3707 )
3708 .await;
3709 let previous = self.wasm_webrtc_carrier_attempts.borrow_mut().insert(
3710 connection_id.clone(),
3711 WasmWebRtcCarrierAttempt {
3712 connection_id: connection_id.clone(),
3713 remote_endpoint_id: endpoint_id,
3714 bootstrap: bootstrap.clone(),
3715 generation,
3716 role: "responder",
3717 prepared: false,
3718 retry_sent: false,
3719 offer_started: false,
3720 remote_ready: false,
3721 completion_started: false,
3722 retry_count: bootstrap.attempt.saturating_sub(1),
3723 inbound_authorization_expires_at_ms: Some(authorization_expiry),
3724 },
3725 );
3726 if let Some(previous) = previous {
3727 self.wasm_webrtc_carrier_sessions
3728 .borrow_mut()
3729 .remove(&previous.bootstrap.upgrade_id);
3730 }
3731 emit_wasm_carrier_action(
3732 &self.wasm_carrier_action_handler,
3733 serde_json::json!({
3734 "type": "prepare-webrtc",
3735 "connectionId": connection_id,
3736 "remoteEndpointId": remote_endpoint_id,
3737 "role": "responder",
3738 "upgradeId": bootstrap.upgrade_id,
3739 "carrierSessionId": bootstrap.carrier_session_id,
3740 "transportGeneration": generation.transport_generation.saturating_add(1),
3741 }),
3742 );
3743 self.schedule_wasm_webrtc_carrier_watchdog(bootstrap);
3744 }
3745 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Ready => {
3746 let upgrade_id = {
3747 let mut attempts = self.wasm_webrtc_carrier_attempts.borrow_mut();
3748 attempts
3749 .get_mut(&connection_id)
3750 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3751 .map(|attempt| {
3752 attempt.remote_ready = true;
3753 attempt.bootstrap.upgrade_id.clone()
3754 })
3755 };
3756 let attempt = upgrade_id.as_deref().and_then(|upgrade_id| {
3757 self.take_ready_outbound_wasm_webrtc_carrier_attempt(
3758 &connection_id,
3759 upgrade_id,
3760 )
3761 });
3762 if let Some(attempt) = attempt {
3763 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
3764 }
3765 }
3766 crate::iroh_carrier_bootstrap::CarrierBootstrapAction::Failed => {
3767 let attempt = self
3768 .wasm_webrtc_carrier_attempts
3769 .borrow()
3770 .get(&connection_id)
3771 .filter(|attempt| bootstrap.is_response_to(&attempt.bootstrap))
3772 .cloned();
3773 if let Some(attempt) = attempt {
3774 let failure_code = crate::client::wasm_peer_carrier_failure_code(
3775 bootstrap.failure_code.as_deref(),
3776 );
3777 if crate::client::should_rearm_wasm_carrier_event_retry(
3778 failure_code,
3779 attempt.role == "initiator",
3780 attempt.completion_started,
3781 ) {
3782 if let Some(current) = self
3783 .wasm_webrtc_carrier_attempts
3784 .borrow_mut()
3785 .get_mut(&connection_id)
3786 .filter(|current| {
3787 current.bootstrap.upgrade_id == attempt.bootstrap.upgrade_id
3788 && !current.completion_started
3789 })
3790 {
3791 current.retry_sent = false;
3792 }
3793 return Ok(true);
3794 }
3795 let should_retry = attempt.role == "initiator"
3796 && attempt.retry_count == 0
3797 && failure_code == "carrier-base-generation-stale";
3798 let remote_endpoint_id = attempt.remote_endpoint_id.to_string();
3799 self.fail_wasm_webrtc_carrier_attempt(attempt, failure_code, false)
3800 .await;
3801 if should_retry {
3802 gloo_timers::future::sleep(std::time::Duration::from_millis(500)).await;
3803 let _ = self
3804 .begin_iroh_webrtc_carrier_attempt(
3805 connection_id,
3806 remote_endpoint_id,
3807 1,
3808 )
3809 .await;
3810 }
3811 }
3812 }
3813 }
3814 Ok(true)
3815 }
3816
3817 #[cfg(all(feature = "transport-moq", not(feature = "transport-webrtc")))]
3818 #[wasm_bindgen(js_name = __handleIrohCarrierControl)]
3819 pub async fn handle_iroh_carrier_control_moq_only(
3820 &self,
3821 connection_id: String,
3822 remote_endpoint_id: String,
3823 frame: JsValue,
3824 ) -> Result<bool, JsValue> {
3825 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3826 let frame: serde_json::Value = serde_wasm_bindgen::from_value(frame)
3827 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3828 let Some(bootstrap) =
3829 crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::from_json(&frame)
3830 else {
3831 return Ok(false);
3832 };
3833 if bootstrap.carrier != crate::iroh_carrier_bootstrap::CarrierBootstrapKind::MoqDraft14
3834 {
3835 return Ok(false);
3836 }
3837 self.handle_wasm_moq_bootstrap(connection_id, remote_endpoint_id, bootstrap)
3838 .await
3839 }
3840
3841 #[cfg(feature = "transport-webrtc")]
3845 #[wasm_bindgen(js_name = __attachWebRtcCarrier)]
3846 pub async fn attach_iroh_webrtc_carrier(
3847 &self,
3848 connection_id: String,
3849 remote_endpoint_id: String,
3850 channel: web_sys::RtcDataChannel,
3851 upgrade_id: String,
3852 ) -> Result<(), JsValue> {
3853 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3854 let endpoint_id = remote_endpoint_id
3855 .parse::<iroh::EndpointId>()
3856 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3857 let attempt = self
3858 .wasm_webrtc_carrier_attempts
3859 .borrow()
3860 .get(&connection_id)
3861 .filter(|attempt| {
3862 attempt.bootstrap.upgrade_id == upgrade_id
3863 && attempt.remote_endpoint_id == endpoint_id
3864 })
3865 .cloned()
3866 .ok_or_else(|| JsValue::from_str("WebRTC carrier attempt is stale"))?;
3867 if !self
3868 .inner
3869 .wasm_carrier_upgrade_is_current(
3870 &connection_id,
3871 crate::client::IrohPathKind::WebRtc,
3872 &upgrade_id,
3873 attempt.generation,
3874 )
3875 .await
3876 {
3877 return Err(JsValue::from_str("WebRTC carrier attempt was retired"));
3878 }
3879 let packet_session = self
3880 .inner
3881 .activate_iroh_packet_carrier(
3882 crate::iroh_carrier_kind::IrohCarrierKind::WebRtc,
3883 endpoint_id,
3884 )
3885 .await
3886 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3887 let expected = attempt
3888 .bootstrap
3889 .frame_expectation()
3890 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3891 let application_key = self
3892 .inner
3893 .application_crypto_key_for_connection(Some(&connection_id))
3894 .ok_or_else(|| {
3895 JsValue::from_str("WebRTC carrier requires the admitted application crypto key")
3896 })?;
3897 let terminal_client = self.inner.clone();
3898 let terminal_connection_id = connection_id.clone();
3899 let terminal_upgrade_id = upgrade_id.clone();
3900 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
3901 let terminal_attempts = self.wasm_webrtc_carrier_attempts.clone();
3902 let terminal_sessions = self.wasm_webrtc_carrier_sessions.clone();
3903 let terminal_handler = self.wasm_carrier_action_handler.clone();
3904 let terminal_lifecycle = self.wasm_carrier_peer_lifecycle.clone();
3905 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
3906 let client = terminal_client.clone();
3907 let connection_id = terminal_connection_id.clone();
3908 let upgrade_id = terminal_upgrade_id.clone();
3909 let attempts = terminal_attempts.clone();
3910 let sessions = terminal_sessions.clone();
3911 let handler = terminal_handler.clone();
3912 let lifecycle = terminal_lifecycle.clone();
3913 spawn_local(async move {
3914 let _lifecycle = lifecycle.lock().await;
3915 if !client
3916 .close_current_iroh_carrier_generation_with_reason(
3917 &connection_id,
3918 endpoint_id,
3919 crate::client::IrohPathKind::WebRtc,
3920 terminal_generation,
3921 reason,
3922 )
3923 .await
3924 {
3925 return;
3926 }
3927 if attempts
3935 .borrow()
3936 .get(&connection_id)
3937 .is_some_and(|current| current.bootstrap.upgrade_id == upgrade_id)
3938 {
3939 attempts.borrow_mut().remove(&connection_id);
3940 }
3941 sessions.borrow_mut().remove(&upgrade_id);
3942 emit_wasm_carrier_action(
3943 &handler,
3944 serde_json::json!({
3945 "type": "retire-webrtc",
3946 "connectionId": connection_id,
3947 "upgradeId": upgrade_id,
3948 "failureCode": reason,
3949 }),
3950 );
3951 });
3952 });
3953 let carrier = crate::wasm_webrtc_carrier::WasmWebRtcCarrierSession::attach(
3954 channel,
3955 packet_session,
3956 expected,
3957 application_key,
3958 on_terminal,
3959 )?;
3960 self.wasm_webrtc_carrier_sessions
3961 .borrow_mut()
3962 .insert(upgrade_id.clone(), carrier);
3963 if attempt.role == "responder" {
3964 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
3965 &attempt.bootstrap,
3966 )
3967 .map_err(|error| JsValue::from_str(&error.to_string()))?;
3968 emit_wasm_carrier_action(
3969 &self.wasm_carrier_action_handler,
3970 serde_json::json!({
3971 "type": "send-control",
3972 "connectionId": connection_id,
3973 "remoteEndpointId": remote_endpoint_id,
3974 "envelope": ready,
3975 }),
3976 );
3977 self.spawn_inbound_wasm_webrtc_carrier_completion(attempt);
3978 } else if let Some(attempt) =
3979 self.take_ready_outbound_wasm_webrtc_carrier_attempt(&connection_id, &upgrade_id)
3980 {
3981 self.spawn_outbound_wasm_webrtc_carrier_completion(attempt);
3982 }
3983 Ok(())
3984 }
3985
3986 #[cfg(feature = "transport-moq")]
3990 #[wasm_bindgen(js_name = __attachMoqCarrier)]
3991 pub async fn attach_iroh_moq_carrier(
3992 &self,
3993 connection_id: String,
3994 remote_endpoint_id: String,
3995 datagrams: JsValue,
3996 upgrade_id: String,
3997 ) -> Result<(), JsValue> {
3998 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
3999 let endpoint_id = remote_endpoint_id
4000 .parse::<iroh::EndpointId>()
4001 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4002 let attempt = self
4003 .wasm_moq_carrier_attempts
4004 .borrow()
4005 .get(&connection_id)
4006 .filter(|attempt| {
4007 attempt.bootstrap.upgrade_id == upgrade_id
4008 && attempt.remote_endpoint_id == endpoint_id
4009 })
4010 .cloned()
4011 .ok_or_else(|| JsValue::from_str("MoQ carrier attempt is stale"))?;
4012 if !self
4013 .inner
4014 .wasm_carrier_upgrade_is_current(
4015 &connection_id,
4016 crate::client::IrohPathKind::Moq,
4017 &upgrade_id,
4018 attempt.generation,
4019 )
4020 .await
4021 {
4022 return Err(JsValue::from_str("MoQ carrier attempt was retired"));
4023 }
4024 let packet_session = self
4025 .inner
4026 .activate_iroh_packet_carrier(
4027 crate::iroh_carrier_kind::IrohCarrierKind::Moq,
4028 endpoint_id,
4029 )
4030 .await
4031 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4032 let expected = attempt
4033 .bootstrap
4034 .frame_expectation()
4035 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4036 let application_key = self
4037 .inner
4038 .application_crypto_key_for_connection(Some(&connection_id))
4039 .ok_or_else(|| {
4040 JsValue::from_str("MoQ carrier requires an installed application key")
4041 })?;
4042 let terminal_client = self.inner.clone();
4043 let terminal_connection_id = connection_id.clone();
4044 let terminal_upgrade_id = upgrade_id.clone();
4045 let terminal_generation = attempt.generation.transport_generation.saturating_add(1);
4046 let terminal_attempts = self.wasm_moq_carrier_attempts.clone();
4047 let terminal_sessions = self.wasm_moq_carrier_sessions.clone();
4048 let terminal_handler = self.wasm_carrier_action_handler.clone();
4049 let terminal_lifecycle = self.wasm_carrier_peer_lifecycle.clone();
4050 let on_terminal: Rc<dyn Fn(&'static str)> = Rc::new(move |reason| {
4051 let client = terminal_client.clone();
4052 let connection_id = terminal_connection_id.clone();
4053 let upgrade_id = terminal_upgrade_id.clone();
4054 let attempts = terminal_attempts.clone();
4055 let sessions = terminal_sessions.clone();
4056 let handler = terminal_handler.clone();
4057 let lifecycle = terminal_lifecycle.clone();
4058 spawn_local(async move {
4059 let _lifecycle = lifecycle.lock().await;
4060 if !client
4061 .close_current_iroh_carrier_generation_with_reason(
4062 &connection_id,
4063 endpoint_id,
4064 crate::client::IrohPathKind::Moq,
4065 terminal_generation,
4066 reason,
4067 )
4068 .await
4069 {
4070 return;
4071 }
4072 if attempts
4073 .borrow()
4074 .get(&connection_id)
4075 .is_some_and(|current| current.bootstrap.upgrade_id == upgrade_id)
4076 {
4077 attempts.borrow_mut().remove(&connection_id);
4078 }
4079 sessions.borrow_mut().remove(&upgrade_id);
4080 emit_wasm_carrier_action(
4081 &handler,
4082 serde_json::json!({
4083 "type": "retire-moq",
4084 "connectionId": connection_id,
4085 "upgradeId": upgrade_id,
4086 "failureCode": reason,
4087 }),
4088 );
4089 });
4090 });
4091 let carrier = crate::wasm_moq_carrier::WasmMoqCarrierSession::attach(
4092 datagrams,
4093 packet_session,
4094 expected,
4095 application_key,
4096 on_terminal,
4097 )?;
4098 if attempt.role == "initiator" {
4099 carrier
4100 .wait_for_peer_data_bidirectional_readiness(std::time::Duration::from_secs(15))
4101 .await?;
4102 }
4103 self.wasm_moq_carrier_sessions
4104 .borrow_mut()
4105 .insert(upgrade_id.clone(), carrier);
4106 if attempt.role == "initiator" {
4107 self.spawn_outbound_wasm_moq_carrier_completion(attempt);
4108 } else {
4109 let ready = crate::iroh_carrier_bootstrap::CarrierBootstrapFrame::ready_from(
4110 &attempt.bootstrap,
4111 )
4112 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4113 emit_wasm_carrier_action(
4114 &self.wasm_carrier_action_handler,
4115 serde_json::json!({
4116 "type": "send-control",
4117 "connectionId": connection_id,
4118 "remoteEndpointId": attempt.remote_endpoint_id.to_string(),
4119 "envelope": ready,
4120 }),
4121 );
4122 self.spawn_inbound_wasm_moq_carrier_completion(attempt);
4123 }
4124 Ok(())
4125 }
4126
4127 #[wasm_bindgen(js_name = setIdentityCredential)]
4128 pub fn set_identity_credential(&self, credential: Option<String>) {
4129 let has_credential = credential
4130 .as_ref()
4131 .map(|value| !value.is_empty())
4132 .unwrap_or(false);
4133 let credential_len = credential.as_ref().map(|value| value.len()).unwrap_or(0);
4134
4135 if let Ok(mut guard) = self.identity_credential.lock() {
4136 *guard = credential.filter(|value| !value.is_empty());
4137 }
4138
4139 let should_log = if let Ok(mut guard) = self.last_auth_log.lock() {
4140 let next = (has_credential, credential_len);
4141 if guard.as_ref() == Some(&next) {
4142 false
4143 } else {
4144 *guard = Some(next);
4145 true
4146 }
4147 } else {
4148 true
4149 };
4150
4151 if should_log {
4152 web_sys::console::log_1(&JsValue::from_str(&format!(
4153 "[OPENRTC][WASM-IDENTITY] credential updated present={} len={}",
4154 has_credential, credential_len
4155 )));
4156 }
4157 }
4158
4159 #[wasm_bindgen(js_name = clearIdentityCredential)]
4165 pub fn clear_identity_credential(&self) {
4166 self.set_identity_credential(None);
4167 }
4168
4169 #[wasm_bindgen(js_name = rankRoutes)]
4172 pub fn rank_routes(
4173 &self,
4174 configured_priority: Vec<String>,
4175 candidates: Vec<String>,
4176 ) -> Vec<String> {
4177 crate::route_policy::rank_routes(&configured_priority, &candidates)
4178 }
4179
4180 #[wasm_bindgen(js_name = setRelay)]
4182 pub async fn set_relay(&self, enabled: bool) -> Result<(), JsValue> {
4183 self.inner
4184 .set_relay(enabled)
4185 .await
4186 .map_err(|error| JsValue::from_str(&error.to_string()))
4187 }
4188
4189 pub async fn init_iroh(&self, secret_key: Option<Vec<u8>>) -> Result<String, JsValue> {
4190 let started_at = js_sys::Date::now();
4191 web_sys::console::log_1(&JsValue::from_str(&format!(
4192 "[OPENRTC][WASM-API] init_iroh called has_secret_key={} secret_key_len={}",
4193 secret_key.as_ref().is_some(),
4194 secret_key.as_ref().map(|k| k.len()).unwrap_or(0)
4195 )));
4196 match self.inner.init_iroh(secret_key, vec![]).await {
4197 Ok(node_id) => {
4198 self.inner.clone().start_wasm_accept_bridge();
4199 let elapsed = js_sys::Date::now() - started_at;
4200 web_sys::console::log_1(&JsValue::from_str(&format!(
4201 "[OPENRTC][WASM-API] init_iroh success elapsed_ms={:.0} node_id={}",
4202 elapsed, node_id
4203 )));
4204 Ok(node_id)
4205 }
4206 Err(err) => {
4207 let elapsed = js_sys::Date::now() - started_at;
4208 web_sys::console::error_1(&JsValue::from_str(&format!(
4209 "[OPENRTC][WASM-API] init_iroh failed elapsed_ms={:.0} error={}",
4210 elapsed, err
4211 )));
4212 Err(JsValue::from_str(&err.to_string()))
4213 }
4214 }
4215 }
4216
4217 #[wasm_bindgen(js_name = initIrohWithTestRelay)]
4220 pub async fn init_iroh_with_test_relay(
4221 &self,
4222 secret_key: Option<Vec<u8>>,
4223 test_relay_url: Option<String>,
4224 ) -> Result<String, JsValue> {
4225 let node_id = self
4226 .inner
4227 .init_iroh_with_test_relay(secret_key, vec![], test_relay_url.as_deref())
4228 .await
4229 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4230 self.inner.clone().start_wasm_accept_bridge();
4234 Ok(node_id)
4235 }
4236
4237 pub async fn iroh_secret_key(&self) -> Result<Vec<u8>, JsValue> {
4238 let node_guard = self.inner.iroh_node.read().await;
4239 if let Some(node) = node_guard.as_ref() {
4240 Ok(node.secret_key())
4241 } else {
4242 Err(JsValue::from_str("Iroh node not initialized"))
4243 }
4244 }
4245
4246 pub async fn node_addr(&self) -> Result<String, JsValue> {
4247 let node_guard = self.inner.iroh_node.read().await;
4248 if let Some(node) = node_guard.as_ref() {
4249 let addr = node
4250 .node_addr()
4251 .await
4252 .map_err(|e| JsValue::from_str(&e.to_string()))?;
4253 serde_json::to_string(&addr).map_err(|e| JsValue::from_str(&e.to_string()))
4254 } else {
4255 Err(JsValue::from_str("Iroh node not initialized"))
4256 }
4257 }
4258
4259 pub async fn endpoint_ticket(&self) -> Result<String, JsValue> {
4260 self.inner
4261 .endpoint_ticket()
4262 .await
4263 .map_err(|e| JsValue::from_str(&e.to_string()))
4264 }
4265
4266 pub async fn endpoint_ticket_with_token(
4270 &self,
4271 grant_scope: String,
4272 max_connections: u32,
4273 ) -> Result<String, JsValue> {
4274 self.inner
4275 .endpoint_ticket_with_token(&grant_scope, max_connections)
4276 .await
4277 .map_err(|e| JsValue::from_str(&e.to_string()))
4278 }
4279
4280 pub fn register_session_token(
4282 &self,
4283 token: String,
4284 grant_scope: String,
4285 max_connections: u32,
4286 ) {
4287 self.inner
4288 .register_session_token(token, grant_scope, max_connections);
4289 }
4290
4291 pub fn register_token_until(
4293 &self,
4294 token: String,
4295 grant_scope: String,
4296 max_connections: u32,
4297 expires_at_ms: u64,
4298 ) {
4299 self.inner
4300 .register_token_until(token, grant_scope, max_connections, expires_at_ms);
4301 }
4302
4303 #[wasm_bindgen(js_name = setConnectionApplicationCryptoRequired)]
4305 pub fn set_connection_application_crypto_required(
4306 &self,
4307 connection_id: String,
4308 ) -> Result<(), JsValue> {
4309 self.inner
4310 .set_connection_application_crypto_required(&connection_id);
4311 Ok(())
4312 }
4313
4314 #[wasm_bindgen(js_name = setConnectionApplicationCryptoKey)]
4316 pub async fn set_connection_application_crypto_key(
4317 &self,
4318 connection_id: String,
4319 key: Vec<u8>,
4320 ) -> Result<(), JsValue> {
4321 if key.len() != crate::application_crypto::APPLICATION_KEY_BYTES {
4322 return Err(JsValue::from_str("application crypto key must be 32 bytes"));
4323 }
4324 let mut key_bytes = [0u8; crate::application_crypto::APPLICATION_KEY_BYTES];
4325 key_bytes.copy_from_slice(&key);
4326 self.inner
4327 .set_connection_application_crypto_key(&connection_id, key_bytes);
4328 self.inner
4329 .emit_current_wasm_connection_state(&connection_id)
4330 .await;
4331 Ok(())
4332 }
4333
4334 #[wasm_bindgen(js_name = clearConnectionApplicationCryptoKey)]
4339 pub fn clear_connection_application_crypto_key(&self, connection_id: String) {
4340 self.inner
4341 .clear_connection_application_crypto_key(&connection_id);
4342 }
4343
4344 #[wasm_bindgen(js_name = ensureConnectionApplicationCrypto)]
4348 pub async fn ensure_connection_application_crypto(
4349 &self,
4350 connection_id: String,
4351 remote_node_id: String,
4352 timeout_ms: u64,
4353 send_frame: js_sys::Function,
4354 ) -> Result<(), JsValue> {
4355 let connection_id = connection_id.trim().to_string();
4356 if connection_id.is_empty() {
4357 return Err(JsValue::from_str("connection id is required"));
4358 }
4359 let remote_node_id = remote_node_id.trim().to_string();
4360 if remote_node_id.is_empty() {
4361 return Err(JsValue::from_str("remote node id is required"));
4362 }
4363 let remote_endpoint_id: iroh::EndpointId = remote_node_id.parse().map_err(|error| {
4364 JsValue::from_str(&format!("invalid remote endpoint id: {error}"))
4365 })?;
4366 self.inner
4372 .ensure_connection_manager_record_before_peer_stream(&remote_endpoint_id)
4373 .await
4374 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4375 let timeout_ms = timeout_ms.max(1);
4376 let initial = self
4377 .inner
4378 .connection_manager
4379 .get_by_connection_id(&connection_id)
4380 .await
4381 .ok_or_else(|| JsValue::from_str("connection is not registered"))?;
4382 let initial_endpoint = initial
4383 .endpoint_id
4384 .as_deref()
4385 .or(initial.node_id.as_deref())
4386 .map(ToOwned::to_owned)
4387 .ok_or_else(|| JsValue::from_str("connection has no endpoint"))?;
4388 let initial_transport_stable_id = initial.transport_stable_id;
4389 let initial_transport_generation = initial.transport_generation;
4390 let initial_route_generation = initial.route_generation;
4391 self.inner
4392 .set_connection_application_crypto_required(&connection_id);
4393
4394 let started_at_ms = js_sys::Date::now();
4395 let retry_delays_ms = [0_u64, 250, 750];
4396 for (attempt, delay_ms) in retry_delays_ms.into_iter().enumerate() {
4397 if delay_ms > 0 {
4398 let remaining_ms = timeout_ms
4399 .saturating_sub((js_sys::Date::now() - started_at_ms).max(0.0) as u64);
4400 if remaining_ms == 0 {
4401 break;
4402 }
4403 gloo_timers::future::sleep(std::time::Duration::from_millis(
4404 delay_ms.min(remaining_ms),
4405 ))
4406 .await;
4407 }
4408 self.inner
4409 .assert_application_crypto_generation(
4410 &connection_id,
4411 &initial_endpoint,
4412 initial_transport_stable_id,
4413 initial_transport_generation,
4414 initial_route_generation,
4415 )
4416 .await
4417 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4418 if self
4419 .inner
4420 .connection_application_crypto_key(&connection_id)
4421 .is_some()
4422 && self.inner.connection_application_crypto_is_confirmed(
4423 &connection_id,
4424 initial_transport_stable_id,
4425 )
4426 {
4427 let confirmed_transport_stable_id =
4428 initial_transport_stable_id.ok_or_else(|| {
4429 JsValue::from_str("confirmed connection has no stable ID")
4430 })?;
4431 if !self
4432 .inner
4433 .confirm_managed_connection_readiness_from_transport_proof(
4434 &connection_id,
4435 confirmed_transport_stable_id,
4436 )
4437 .await
4438 {
4439 return Err(JsValue::from_str(
4440 "confirmed application route belongs to a retired generation",
4441 ));
4442 }
4443 self.inner
4444 .emit_current_wasm_connection_state(&connection_id)
4445 .await;
4446 return Ok(());
4447 }
4448 let frame = self
4449 .inner
4450 .application_key_handshake_frame(&connection_id, "capability-update")
4451 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4452 let frame = serde::Serialize::serialize(
4453 &frame,
4454 &serde_wasm_bindgen::Serializer::json_compatible(),
4455 )
4456 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4457 let send_result = send_frame.call1(&JsValue::NULL, &frame)?;
4458 wasm_bindgen_futures::JsFuture::from(js_sys::Promise::resolve(&send_result))
4459 .await?;
4460
4461 let attempt_deadline_ms = ((attempt + 1) as u64 * timeout_ms / 3).max(1);
4462 loop {
4463 if self
4464 .inner
4465 .connection_application_crypto_key(&connection_id)
4466 .is_some()
4467 && self.inner.connection_application_crypto_is_confirmed(
4468 &connection_id,
4469 initial_transport_stable_id,
4470 )
4471 {
4472 let confirmed_transport_stable_id = initial_transport_stable_id
4473 .ok_or_else(|| {
4474 JsValue::from_str("confirmed connection has no stable ID")
4475 })?;
4476 if !self
4477 .inner
4478 .confirm_managed_connection_readiness_from_transport_proof(
4479 &connection_id,
4480 confirmed_transport_stable_id,
4481 )
4482 .await
4483 {
4484 return Err(JsValue::from_str(
4485 "confirmed application route belongs to a retired generation",
4486 ));
4487 }
4488 self.inner
4489 .emit_current_wasm_connection_state(&connection_id)
4490 .await;
4491 return Ok(());
4492 }
4493 let elapsed_ms = (js_sys::Date::now() - started_at_ms).max(0.0) as u64;
4494 if elapsed_ms >= attempt_deadline_ms || elapsed_ms >= timeout_ms {
4495 break;
4496 }
4497 self.inner
4498 .assert_application_crypto_generation(
4499 &connection_id,
4500 &initial_endpoint,
4501 initial_transport_stable_id,
4502 initial_transport_generation,
4503 initial_route_generation,
4504 )
4505 .await
4506 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4507 gloo_timers::future::sleep(std::time::Duration::from_millis(25)).await;
4508 }
4509 }
4510 let current_manager_transport_stable_id = self
4511 .inner
4512 .connection_manager
4513 .get_by_connection_id(&connection_id)
4514 .await
4515 .and_then(|record| record.transport_stable_id);
4516 let current_physical_transport_stable_id = self
4517 .inner
4518 .get_connection(remote_endpoint_id)
4519 .await
4520 .map(|connection| crate::transport_generation::for_connection(&connection));
4521 let confirmation = self
4522 .inner
4523 .connection_application_crypto_confirmation_snapshot(&connection_id);
4524 return Err(JsValue::from_str(&format!(
4525 "connection {} reciprocal application key agreement timed out after {}ms: frozen_stable_id={:?} current_manager_stable_id={:?} current_physical_stable_id={:?} confirmation={:?}",
4526 connection_id,
4527 timeout_ms,
4528 initial_transport_stable_id,
4529 current_manager_transport_stable_id,
4530 current_physical_transport_stable_id,
4531 confirmation,
4532 )));
4533 }
4534
4535 #[wasm_bindgen(js_name = handleConnectionApplicationCryptoHandshake)]
4536 pub async fn handle_connection_application_crypto_handshake(
4537 &self,
4538 connection_id: String,
4539 remote_node_id: String,
4540 transport_stable_id: Option<u64>,
4541 action: Option<String>,
4542 remote_public_key: Vec<u8>,
4543 ) -> Result<JsValue, JsValue> {
4544 if remote_public_key.len() != crate::key_agreement::PUBLIC_KEY_BYTES {
4545 return Err(JsValue::from_str(
4546 "application key agreement public key must be 32 bytes",
4547 ));
4548 }
4549 let mut remote_public = [0_u8; crate::key_agreement::PUBLIC_KEY_BYTES];
4550 remote_public.copy_from_slice(&remote_public_key);
4551 let outcome = self
4552 .inner
4553 .accept_application_key_handshake(
4554 &connection_id,
4555 &remote_node_id,
4556 transport_stable_id,
4557 action.as_deref(),
4558 remote_public,
4559 )
4560 .await
4561 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4562 let reply_frame = outcome
4563 .reply_action
4564 .map(|reply_action| {
4565 self.inner
4566 .application_key_handshake_frame(&connection_id, reply_action)
4567 })
4568 .transpose()
4569 .map_err(|error| JsValue::from_str(&error.to_string()))?;
4570 self.inner
4571 .emit_current_wasm_connection_state(&connection_id)
4572 .await;
4573 serde::Serialize::serialize(
4574 &reply_frame,
4575 &serde_wasm_bindgen::Serializer::json_compatible(),
4576 )
4577 .map_err(|error| JsValue::from_str(&error.to_string()))
4578 }
4579
4580 #[wasm_bindgen(js_name = connectionApplicationCryptoReady)]
4581 pub fn connection_application_crypto_ready(
4582 &self,
4583 connection_id: String,
4584 transport_stable_id: u64,
4585 ) -> bool {
4586 self.inner
4587 .connection_application_crypto_key(&connection_id)
4588 .is_some()
4589 && self.inner.connection_application_crypto_is_confirmed(
4590 &connection_id,
4591 Some(transport_stable_id),
4592 )
4593 }
4594
4595 #[wasm_bindgen(js_name = protectConnectionApplicationPayload)]
4596 pub fn protect_connection_application_payload(
4597 &self,
4598 connection_id: String,
4599 type_id: u8,
4600 payload: Vec<u8>,
4601 ) -> Result<Vec<u8>, JsValue> {
4602 self.inner
4603 .protect_outbound_application_payload_with_type(&connection_id, type_id, &payload)
4604 .map_err(|error| JsValue::from_str(&error.to_string()))
4605 }
4606
4607 #[wasm_bindgen(js_name = openConnectionApplicationPayload)]
4608 pub fn open_connection_application_payload(
4609 &self,
4610 connection_id: String,
4611 type_id: u8,
4612 payload: Vec<u8>,
4613 ) -> Result<Vec<u8>, JsValue> {
4614 self.inner
4615 .open_inbound_application_payload_with_type(&connection_id, type_id, &payload)
4616 .map_err(|error| JsValue::from_str(&error.to_string()))
4617 }
4618
4619 pub fn validate_session_token(&self, token: String) -> Result<String, JsValue> {
4623 self.inner
4624 .validate_session_token(&token)
4625 .map_err(|e| JsValue::from_str(&e))
4626 }
4627
4628 pub async fn validate_connection_token(
4630 &self,
4631 token: String,
4632 connection_id: String,
4633 token_payload: Option<String>,
4634 ) -> Result<String, JsValue> {
4635 self.inner
4636 .validate_connection_token(&token, &connection_id, token_payload.as_deref())
4637 .await
4638 .map_err(|e| JsValue::from_str(&e))
4639 }
4640
4641 pub async fn present_session_token(
4648 &self,
4649 endpoint_id: String,
4650 token: String,
4651 token_payload: Option<String>,
4652 device_id: Option<String>,
4653 ) -> Result<String, JsValue> {
4654 crate::console_log!(
4655 "[OpenRTC][session-admission][wasm-present] endpoint_id={} claimed_local_device_id={}",
4656 endpoint_id,
4657 device_id.as_deref().unwrap_or("<none>")
4658 );
4659 let endpoint_id_parsed: iroh::EndpointId = endpoint_id
4660 .parse()
4661 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
4662 let local_node_id = self.inner.current_node_id().await.ok_or_else(|| {
4663 JsValue::from_str("missing local node id for session-token presentation")
4664 })?;
4665 let connection_id =
4666 crate::client::Client::deterministic_connection_id(&local_node_id, &endpoint_id);
4667 let approval_scope = self
4668 .inner
4669 .present_and_accept_session_token_with_local_claim(
4670 endpoint_id_parsed,
4671 &connection_id,
4672 &token,
4673 token_payload.as_deref(),
4674 None,
4675 device_id,
4676 )
4677 .await
4678 .map_err(|e| JsValue::from_str(&e))?;
4679 self.inner
4680 .emit_current_wasm_connection_state(&connection_id)
4681 .await;
4682
4683 Ok(approval_scope)
4684 }
4685
4686 pub async fn is_remote_admitted(&self, endpoint_ticket: String) -> Result<bool, JsValue> {
4689 self.inner
4690 .is_remote_admitted(&endpoint_ticket)
4691 .await
4692 .map_err(|error| JsValue::from_str(&error.to_string()))
4693 }
4694
4695 #[allow(clippy::too_many_arguments)]
4698 pub async fn prepare_reciprocal_admission(
4699 &self,
4700 endpoint_id: String,
4701 expected_transport_stable_id: u64,
4702 stream_instance_id: String,
4703 presentation_id: String,
4704 token: String,
4705 token_payload: String,
4706 device_id: String,
4707 stream_contract: String,
4708 ) -> Result<bool, JsValue> {
4709 let endpoint_id: iroh::EndpointId = endpoint_id
4710 .parse()
4711 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
4712 let stream_contract = match stream_contract.trim() {
4713 "one-shot-admission" => {
4714 crate::native_protocol::TokenStreamContract::OneShotAdmission
4715 }
4716 "persistent-control" => {
4717 crate::native_protocol::TokenStreamContract::PersistentControl
4718 }
4719 other => {
4720 return Err(JsValue::from_str(&format!(
4721 "unsupported reciprocal stream contract: {other}"
4722 )))
4723 }
4724 };
4725 self.inner
4726 .prepare_reciprocal_admission(
4727 endpoint_id,
4728 expected_transport_stable_id,
4729 stream_instance_id.as_str(),
4730 presentation_id.as_str(),
4731 token.as_str(),
4732 token_payload.as_str(),
4733 device_id.as_str(),
4734 stream_contract,
4735 )
4736 .await
4737 .map_err(|error| JsValue::from_str(&error))?;
4738 Ok(true)
4739 }
4740
4741 pub async fn confirm_reciprocal_admission(
4744 &self,
4745 endpoint_id: String,
4746 expected_transport_stable_id: u64,
4747 stream_instance_id: String,
4748 presentation_id: String,
4749 accepted: bool,
4750 approval_scope: Option<String>,
4751 ) -> Result<bool, JsValue> {
4752 let endpoint_id: iroh::EndpointId = endpoint_id
4753 .parse()
4754 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
4755 self.inner
4756 .confirm_reciprocal_admission(
4757 endpoint_id,
4758 expected_transport_stable_id,
4759 stream_instance_id.as_str(),
4760 presentation_id.as_str(),
4761 accepted,
4762 approval_scope.as_deref(),
4763 )
4764 .await
4765 .map_err(|error| JsValue::from_str(&error))?;
4766 self.inner
4767 .emit_current_wasm_connection_state(
4768 &crate::client::Client::deterministic_connection_id(
4769 &self.inner.current_node_id().await.ok_or_else(|| {
4770 JsValue::from_str(
4771 "missing local node id after reciprocal admission ACK",
4772 )
4773 })?,
4774 &endpoint_id.to_string(),
4775 ),
4776 )
4777 .await;
4778 Ok(true)
4779 }
4780
4781 pub fn revoke_session_token(&self, token: String) -> Result<JsValue, JsValue> {
4783 serde_wasm_bindgen::to_value(&self.inner.revoke_session_token(&token))
4784 .map_err(|error| JsValue::from_str(&error.to_string()))
4785 }
4786
4787 pub async fn revoke_tokens_by_scope(
4789 &self,
4790 grant_scope: String,
4791 ) -> Result<JsValue, JsValue> {
4792 let affected = self.inner.begin_revoke_tokens_by_scope(&grant_scope);
4793 for connection_id in &affected {
4798 #[cfg(not(any(feature = "transport-webrtc", feature = "transport-moq")))]
4799 let _ = connection_id;
4800 #[cfg(feature = "transport-webrtc")]
4801 self.retire_iroh_webrtc_carrier(
4802 connection_id.clone(),
4803 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string()),
4804 )
4805 .await;
4806 #[cfg(feature = "transport-moq")]
4807 self.retire_iroh_moq_carrier(
4808 connection_id.clone(),
4809 Some(crate::lifecycle_reason::REASON_SESSION_TOKEN_REVOKED.to_string()),
4810 )
4811 .await;
4812 }
4813 self.inner
4814 .finish_revoke_tokens_by_scope(&grant_scope, &affected)
4815 .await;
4816 serde_wasm_bindgen::to_value(&affected).map_err(|e| JsValue::from_str(&e.to_string()))
4817 }
4818
4819 pub fn clear_session_tokens(&self) {
4821 self.inner.clear_session_tokens();
4822 }
4823
4824 pub fn endpoint_id_from_ticket(&self, ticket: String) -> Result<String, JsValue> {
4825 let (iroh_ticket, _token_suffix) = split_ticket(ticket.trim());
4826 let parsed = EndpointTicket::from_str(iroh_ticket)
4827 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
4828 Ok(parsed.endpoint_addr().id.to_string())
4829 }
4830
4831 pub async fn disconnect(&self, endpoint_id: String) -> Result<(), JsValue> {
4832 let endpoint_id: iroh::EndpointId = endpoint_id
4833 .parse()
4834 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
4835 let node_guard = self.inner.iroh_node.read().await;
4836 if let Some(node) = node_guard.as_ref() {
4837 node.disconnect(endpoint_id)
4838 .await
4839 .map_err(|e| JsValue::from_str(&e.to_string()))
4840 } else {
4841 Err(JsValue::from_str("Iroh node not initialized"))
4842 }
4843 }
4844
4845 pub async fn disconnect_transient(&self, endpoint_id: String) -> Result<(), JsValue> {
4858 let endpoint_id: iroh::EndpointId = endpoint_id
4859 .parse()
4860 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
4861 self.inner
4862 .disconnect_with_reason(
4863 endpoint_id,
4864 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
4865 )
4866 .await
4867 .map_err(|e| JsValue::from_str(&e.to_string()))?;
4868 self.inner.wake_browser_auto_connect();
4874 Ok(())
4875 }
4876
4877 pub async fn is_connected(&self, endpoint_id: String) -> Result<bool, JsValue> {
4878 let endpoint_id: iroh::EndpointId = endpoint_id
4879 .parse()
4880 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
4881 Ok(self.inner.is_connected(endpoint_id).await)
4882 }
4883
4884 pub fn runtime_policy(&self) -> Result<JsValue, JsValue> {
4885 serde_wasm_bindgen::to_value(&self.inner.policy_snapshot())
4886 .map_err(|e| JsValue::from_str(&e.to_string()))
4887 }
4888
4889 pub async fn add_peer_scope(&self, id: String, scope: String) -> Result<JsValue, JsValue> {
4890 let scopes = self.inner.add_peer_scope(&id, &scope).await;
4891 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
4892 }
4893
4894 pub async fn release_peer_scope(
4895 &self,
4896 id: String,
4897 scope: Option<String>,
4898 ) -> Result<JsValue, JsValue> {
4899 let scopes = self.inner.release_peer_scope(&id, scope.as_deref()).await;
4900 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
4901 }
4902
4903 pub async fn peer_scopes(&self, id: String) -> Result<JsValue, JsValue> {
4904 let scopes = self.inner.peer_scopes(&id).await;
4905 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
4906 }
4907
4908 pub async fn same_peer(&self, left: String, right: String) -> Result<bool, JsValue> {
4909 Ok(self.inner.same_peer(&left, &right).await)
4910 }
4911
4912 pub async fn peer_snapshot(&self, id: String) -> Result<JsValue, JsValue> {
4913 let snapshot = self.inner.peer_snapshot(&id).await;
4914 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
4915 }
4916
4917 pub async fn peer_session(&self, id: String) -> Result<JsValue, JsValue> {
4921 let snapshot = self.inner.peer_session(&id).await;
4922 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
4923 }
4924
4925 pub async fn peer_sessions(&self) -> Result<JsValue, JsValue> {
4926 let snapshots = self.inner.peer_sessions().await;
4927 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
4928 }
4929
4930 pub async fn connection_state(&self, connection_id: String) -> Result<JsValue, JsValue> {
4931 let snapshot = self.inner.connection_state(&connection_id).await;
4932 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
4933 }
4934
4935 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
4936 async fn revisit_preferred_iroh_carrier_after_settlement(&self, connection_id: &str) {
4937 let _lifecycle = self.wasm_carrier_peer_lifecycle.lock().await;
4938 let Some(record) = self
4939 .inner
4940 .connection_manager
4941 .get_by_connection_id(connection_id)
4942 .await
4943 else {
4944 return;
4945 };
4946 if matches!(
4949 record.active_transport.as_str(),
4950 crate::transport_label::WEBRTC | crate::transport_label::MOQ
4951 ) {
4952 return;
4953 }
4954 let Some(capabilities) = self
4955 .wasm_remote_carrier_capabilities
4956 .borrow()
4957 .get(connection_id)
4958 .copied()
4959 else {
4960 return;
4961 };
4962 let Some(remote_endpoint_id) = self
4963 .inner
4964 .wasm_iroh_carrier_remote_endpoint_id(connection_id)
4965 .await
4966 else {
4967 return;
4968 };
4969 if let Err(error) = self
4970 .begin_preferred_iroh_carrier_locked(
4971 connection_id.to_string(),
4972 remote_endpoint_id,
4973 capabilities.webrtc,
4974 capabilities.moq,
4975 )
4976 .await
4977 {
4978 web_sys::console::warn_1(&JsValue::from_str(&format!(
4979 "[OpenRTC][Iroh carrier] settled-generation revisit failed connection_id={} error={:?}",
4980 connection_id, error,
4981 )));
4982 }
4983 }
4984
4985 pub async fn connection_states(&self) -> Result<JsValue, JsValue> {
4986 let snapshots = self.inner.connection_states().await;
4987 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
4988 }
4989
4990 pub async fn wait_for_peer(
4991 &self,
4992 id: String,
4993 timeout_ms: Option<u32>,
4994 ) -> Result<JsValue, JsValue> {
4995 let snapshot = self
4996 .inner
4997 .wait_for_peer(&id, timeout_ms.map(|value| value as u64))
4998 .await;
4999 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5000 }
5001
5002 pub async fn resolve_peer_connection_records(
5003 &self,
5004 id: String,
5005 ) -> Result<JsValue, JsValue> {
5006 let records = self.inner.resolve_peer_connection_records(&id).await;
5007 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
5008 }
5009
5010 pub async fn list_managed_connections(&self) -> Result<JsValue, JsValue> {
5011 let records = self.inner.list_managed_connections().await;
5012 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
5013 }
5014
5015 pub async fn bind_connection_device_id(
5016 &self,
5017 connection_id: String,
5018 device_id: String,
5019 ) -> Result<JsValue, JsValue> {
5020 let snapshot = self
5021 .inner
5022 .bind_connection_device_id(&connection_id, &device_id)
5023 .await;
5024 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5025 }
5026
5027 pub async fn acknowledge_session_token_response(
5028 &self,
5029 connection_id: String,
5030 remote_node_id: String,
5031 transport_stable_id: u64,
5032 claimed_device_id: Option<String>,
5033 ) -> bool {
5034 self.inner
5035 .acknowledge_wasm_session_token_response(
5036 &connection_id,
5037 &remote_node_id,
5038 transport_stable_id,
5039 claimed_device_id.as_deref(),
5040 )
5041 .await
5042 }
5043
5044 pub async fn bind_node_device_id(
5045 &self,
5046 node_id: String,
5047 device_id: String,
5048 ) -> Result<(), JsValue> {
5049 self.inner.bind_node_device_id(&node_id, &device_id).await;
5050 Ok(())
5051 }
5052
5053 pub async fn reject_connection_admission(
5054 &self,
5055 connection_id: String,
5056 reason: String,
5057 ) -> Result<JsValue, JsValue> {
5058 self.inner
5059 .reject_session_connection(&connection_id, &reason);
5060 self.inner
5061 .emit_current_wasm_connection_state(&connection_id)
5062 .await;
5063 let snapshot = self.inner.connection_state(&connection_id).await;
5064 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5065 }
5066
5067 pub async fn report_managed_connection_settled(
5068 &self,
5069 connection_id: String,
5070 settled: bool,
5071 device_id: Option<String>,
5072 transport_stable_id: Option<u64>,
5073 transport_generation: Option<u64>,
5074 route_generation: Option<u64>,
5075 ) -> Result<JsValue, JsValue> {
5076 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
5077 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
5078 self.inner
5079 .report_managed_connection_settled_for_transport(
5080 &connection_id,
5081 settled,
5082 transport_stable_id,
5083 transport_generation,
5084 route_generation,
5085 )
5086 .await
5087 }
5088 _ => None,
5089 };
5090 let _ = device_id;
5091 #[cfg(any(feature = "transport-webrtc", feature = "transport-moq"))]
5092 if settled && snapshot.is_some() {
5093 self.revisit_preferred_iroh_carrier_after_settlement(&connection_id)
5099 .await;
5100 }
5101 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5102 }
5103
5104 pub async fn report_transport_status(
5105 &self,
5106 connection_id: String,
5107 active_transport: String,
5108 parallel_transport: Option<String>,
5109 transport_stable_id: Option<u64>,
5110 transport_generation: Option<u64>,
5111 route_generation: Option<u64>,
5112 ) -> Result<JsValue, JsValue> {
5113 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
5114 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
5115 self.inner
5116 .report_transport_status_for_generation(
5117 &connection_id,
5118 &active_transport,
5119 parallel_transport.as_deref(),
5120 transport_stable_id,
5121 transport_generation,
5122 route_generation,
5123 )
5124 .await
5125 }
5126 _ => None,
5127 };
5128 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
5129 }
5130
5131 pub async fn is_current_transport_stable_id(
5132 &self,
5133 endpoint_id: String,
5134 transport_stable_id: u64,
5135 ) -> Result<bool, JsValue> {
5136 let endpoint_id = endpoint_id
5137 .parse::<iroh::EndpointId>()
5138 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5139 Ok(self
5140 .inner
5141 .is_current_transport_stable_id(endpoint_id, transport_stable_id)
5142 .await)
5143 }
5144
5145 pub async fn open_bi(&self, endpoint_id: String) -> Result<BiStream, JsValue> {
5146 let endpoint_id: iroh::EndpointId = endpoint_id
5147 .parse()
5148 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5149 self.inner
5150 .assert_raw_peer_stream_allowed(&endpoint_id)
5151 .await
5152 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5153 self.inner
5162 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5163 .await
5164 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5165 let (send, recv) = self
5166 .inner
5167 .open_bi_internal(endpoint_id)
5168 .await
5169 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5170 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
5171 }
5172
5173 pub async fn open_native_main_control_bi(
5202 &self,
5203 endpoint_id: String,
5204 ) -> Result<BiStream, JsValue> {
5205 let endpoint_id: iroh::EndpointId = endpoint_id
5206 .parse()
5207 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5208 self.inner
5209 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5210 .await
5211 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5212 let (transport_stable_id, send, recv) = self
5213 .inner
5214 .open_bi_internal_with_transport_stable_id(endpoint_id)
5215 .await
5216 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5217 Ok(BiStream::from_parts_with_transport_stable_id(
5218 send,
5219 recv,
5220 endpoint_id.to_string(),
5221 transport_stable_id,
5222 ))
5223 }
5224
5225 pub async fn send_native_main_control_frame(
5230 &self,
5231 endpoint_id: String,
5232 frame: Vec<u8>,
5233 ) -> Result<(), JsValue> {
5234 send_native_signal_control_frame(&self.inner, endpoint_id, frame).await
5235 }
5236
5237 pub async fn open_peer_bi(
5238 &self,
5239 id: String,
5240 timeout_ms: Option<u32>,
5241 ) -> Result<BiStream, JsValue> {
5242 let (_connection_id, remote_node_id, send, recv) = self
5243 .inner
5244 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
5245 .await
5246 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5247 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
5248 }
5249
5250 pub async fn send_peer_application_frame(
5254 &self,
5255 id: String,
5256 frame: Vec<u8>,
5257 timeout_ms: Option<u32>,
5258 ) -> Result<(), JsValue> {
5259 self.inner
5260 .send_peer_application_frame(&id, &frame, timeout_ms.map(|value| value as u64))
5261 .await
5262 .map_err(|error| JsValue::from_str(&error.to_string()))
5263 }
5264
5265 pub async fn open_peer_bi_explicit_file_sender(
5271 &self,
5272 id: String,
5273 timeout_ms: Option<u32>,
5274 ) -> Result<PeerUniStream, JsValue> {
5275 let (_connection_id, _remote_node_id, send) = self
5276 .inner
5277 .open_peer_bi_explicit_file_sender(&id, timeout_ms.map(|value| value as u64))
5278 .await
5279 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5280 Ok(peer_uni_stream_from_send(send))
5281 }
5282
5283 pub async fn open_peer_bi_transport_only(
5287 &self,
5288 id: String,
5289 timeout_ms: Option<u32>,
5290 ) -> Result<BiStream, JsValue> {
5291 let (_connection_id, remote_node_id, send, recv) = self
5292 .inner
5293 .open_peer_bi_transport_only(&id, timeout_ms.map(|value| value as u64))
5294 .await
5295 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5296 Ok(BiStream::from_parts(send, recv, remote_node_id))
5297 }
5298
5299 pub async fn open_uni(&self, endpoint_id: String) -> Result<PeerUniStream, JsValue> {
5300 let endpoint_id: iroh::EndpointId = endpoint_id
5301 .parse()
5302 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
5303 self.inner
5304 .assert_raw_peer_stream_allowed(&endpoint_id)
5305 .await
5306 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5307 self.inner
5310 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
5311 .await
5312 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5313 let node_guard = self.inner.iroh_node.read().await;
5314 if let Some(node) = node_guard.as_ref() {
5315 let send = node
5316 .open_uni(endpoint_id.clone())
5317 .await
5318 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5319 Ok(peer_uni_stream_from_send(
5320 crate::application_crypto_streams::PeerSendStream::plain(send),
5321 ))
5322 } else {
5323 Err(JsValue::from_str("Iroh node not initialized"))
5324 }
5325 }
5326
5327 pub async fn open_peer_uni(
5328 &self,
5329 id: String,
5330 timeout_ms: Option<u32>,
5331 ) -> Result<PeerUniStream, JsValue> {
5332 let (_connection_id, _remote_node_id, send) = self
5333 .inner
5334 .open_peer_uni(&id, timeout_ms.map(|value| value as u64))
5335 .await
5336 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5337 Ok(peer_uni_stream_from_send(send))
5338 }
5339
5340 #[wasm_bindgen(js_name = sendPeerDatagram)]
5341 pub async fn send_peer_datagram(
5342 &self,
5343 id: String,
5344 payload: Vec<u8>,
5345 max_age_ms: Option<u32>,
5346 ) -> Result<bool, JsValue> {
5347 match max_age_ms {
5348 Some(max_age_ms) => {
5349 self.inner
5350 .send_peer_datagram_with_max_age(&id, &payload, u64::from(max_age_ms))
5351 .await
5352 }
5353 None => self
5354 .inner
5355 .send_peer_datagram(&id, &payload)
5356 .await
5357 .map(|()| true),
5358 }
5359 .map_err(|error| JsValue::from_str(&error.to_string()))
5360 }
5361
5362 #[wasm_bindgen(js_name = receivePeerDatagram)]
5363 pub async fn receive_peer_datagram(&self, id: String) -> Result<Vec<u8>, JsValue> {
5364 self.inner
5365 .receive_peer_datagram(&id)
5366 .await
5367 .map_err(|error| JsValue::from_str(&error.to_string()))
5368 }
5369
5370 #[wasm_bindgen(js_name = peerDatagramMaxSize)]
5371 pub fn peer_datagram_max_size(&self) -> usize {
5372 Client::MAX_PEER_DATAGRAM_BYTES
5373 }
5374
5375 #[wasm_bindgen(js_name = createPeerDatagramPolicy)]
5376 pub fn create_peer_datagram_policy(&self) -> WasmPeerDatagramPolicy {
5377 WasmPeerDatagramPolicy {
5378 inner: RefCell::new(crate::datagrams::PeerDatagramPolicy::new(
5379 Client::MAX_PEER_DATAGRAM_BYTES,
5380 )),
5381 }
5382 }
5383
5384 pub async fn send_peer(&self, id: String, data: Vec<u8>) -> Result<(), JsValue> {
5394 self.inner
5395 .send_peer(&id, &data)
5396 .await
5397 .map_err(|e| JsValue::from_str(&e.to_string()))
5398 }
5399
5400 pub async fn iroh_path_kind(&self, peer_id: String) -> String {
5404 match self.inner.iroh_path_kind(&peer_id).await {
5405 crate::client::IrohPathKind::DirectQuic => "direct-quic".to_string(),
5406 crate::client::IrohPathKind::DirectLan => "direct-lan".to_string(),
5407 crate::client::IrohPathKind::Relay => "relay".to_string(),
5408 crate::client::IrohPathKind::Ble => "ble".to_string(),
5409 crate::client::IrohPathKind::WebRtc => "webrtc".to_string(),
5410 crate::client::IrohPathKind::Moq => "moq".to_string(),
5411 crate::client::IrohPathKind::Unknown => "unknown".to_string(),
5412 }
5413 }
5414
5415 pub async fn iroh_transport_rtt_ms(&self, peer_id: String) -> Option<u32> {
5418 self.inner
5419 .iroh_transport_rtt_ms(&peer_id)
5420 .await
5421 .map(|value| value.min(u32::MAX as u64) as u32)
5422 }
5423
5424 pub async fn incoming_streams(&self) -> Result<JsReadableStream, JsValue> {
5425 let (stream_owner, stream) = {
5426 let node_guard = self.inner.iroh_node.read().await;
5427 if let Some(node) = node_guard.as_ref() {
5428 (self.inner.clone(), node.incoming_streams_stream())
5429 } else {
5430 return Err(JsValue::from_str("Iroh node not initialized"));
5431 }
5432 };
5433
5434 use futures::StreamExt;
5435 let mapped_stream = stream.filter_map(move |incoming| {
5436 let stream_owner = stream_owner.clone();
5437 async move {
5438 let owned = stream_owner
5439 .incoming_stream_generation_is_owned(
5440 incoming.endpoint_id,
5441 incoming.transport_stable_id,
5442 )
5443 .await;
5444 let admission_candidate = !owned
5445 && matches!(
5446 &incoming.stream,
5447 crate::wasm_node::IncomingStreamType::Bi(_, _)
5448 )
5449 && stream_owner
5450 .incoming_stream_generation_is_authenticated_reconnect_candidate(
5451 incoming.endpoint_id,
5452 incoming.transport_stable_id,
5453 )
5454 .await;
5455 (owned || admission_candidate).then(|| {
5456 crate::wasm_node::BiStream::incoming_to_js_value(
5457 incoming,
5458 admission_candidate,
5459 )
5460 })
5461 }
5462 });
5463
5464 Ok(wasm_streams::ReadableStream::from_stream(mapped_stream).into_raw())
5465 }
5466
5467 #[wasm_bindgen(js_name = inspectIncomingStreamPrefix)]
5471 pub fn inspect_incoming_stream_prefix(
5472 &self,
5473 bytes: Vec<u8>,
5474 reached_eof: bool,
5475 ) -> Result<JsValue, JsValue> {
5476 serde::Serialize::serialize(
5477 &crate::stream_metadata::inspect_prefix_for_binding(&bytes, reached_eof),
5478 &serde_wasm_bindgen::Serializer::json_compatible(),
5479 )
5480 .map_err(|error| JsValue::from_str(&error.to_string()))
5481 }
5482
5483 #[wasm_bindgen(js_name = decodeIncomingChannelEnvelope)]
5487 pub fn decode_incoming_channel_envelope(&self, bytes: Vec<u8>) -> Result<JsValue, JsValue> {
5488 serde::Serialize::serialize(
5489 &crate::stream_metadata::decode_prefix_for_binding(&bytes),
5490 &serde_wasm_bindgen::Serializer::json_compatible(),
5491 )
5492 .map_err(|error| JsValue::from_str(&error.to_string()))
5493 }
5494
5495 pub async fn update_presence(
5496 &self,
5497 user_id: String,
5498 device_name: String,
5499 ticket: String,
5500 metadata: Option<String>,
5501 ttl_ms: Option<u64>,
5502 ) -> Result<(), JsValue> {
5503 self.inner
5504 .update_presence_with_ttl(
5505 &user_id,
5506 &device_name,
5507 &ticket,
5508 ttl_ms.unwrap_or(300_000),
5509 metadata.as_deref(),
5510 )
5511 .await
5512 .map_err(|e| JsValue::from_str(&e.to_string()))
5513 }
5514
5515 pub async fn send_message(
5516 &self,
5517 target_id: String,
5518 payload: String,
5519 state: Option<String>,
5520 reply_payload: Option<String>,
5521 ) -> Result<String, JsValue> {
5522 self.inner
5523 .send_message(
5524 &target_id,
5525 &payload,
5526 state.as_deref(),
5527 reply_payload.as_deref(),
5528 )
5529 .await
5530 .map_err(|e| JsValue::from_str(&e.to_string()))
5531 }
5532
5533 pub async fn set_offline(&self, user_id: String) -> Result<(), JsValue> {
5534 self.inner
5535 .set_offline(&user_id)
5536 .await
5537 .map_err(|e| JsValue::from_str(&e.to_string()))
5538 }
5539
5540 pub async fn update_device(
5541 &self,
5542 user_id: String,
5543 device_id: String,
5544 device_name: Option<String>,
5545 capabilities: Option<JsValue>,
5546 metadata: Option<String>,
5547 ) -> Result<(), JsValue> {
5548 let parsed_capabilities = match capabilities {
5549 Some(value) if !value.is_null() && !value.is_undefined() => Some(
5550 serde_wasm_bindgen::from_value::<crate::signaling::DeviceCapabilities>(value)
5551 .map_err(|e| JsValue::from_str(&e.to_string()))?,
5552 ),
5553 _ => None,
5554 };
5555
5556 self.inner
5557 .update_device(
5558 &user_id,
5559 &device_id,
5560 device_name.as_deref(),
5561 parsed_capabilities,
5562 metadata.as_deref(),
5563 )
5564 .await
5565 .map_err(|e| JsValue::from_str(&e.to_string()))
5566 }
5567
5568 pub async fn delete_device(
5569 &self,
5570 user_id: String,
5571 device_id: String,
5572 ) -> Result<(), JsValue> {
5573 self.inner
5574 .delete_device(&user_id, &device_id)
5575 .await
5576 .map_err(|e| JsValue::from_str(&e.to_string()))
5577 }
5578
5579 pub fn force_reconnect_snapshot(&self) {
5580 self.inner.clone().force_reconnect_snapshot();
5581 }
5582
5583 pub fn stop_presence_loop(&self) {
5584 self.inner.stop_presence_loop();
5585 }
5586
5587 pub fn stop_auto_connect(&self) {
5588 self.inner.stop_auto_connect();
5589 self.inner.stop_browser_auto_connect();
5590 }
5591
5592 pub fn start_auto_connect(
5593 &self,
5594 user_id: String,
5595 local_device_id: String,
5596 ) -> Result<(), JsValue> {
5597 self.inner
5598 .start_browser_auto_connect(user_id, local_device_id)
5599 .map_err(|error| JsValue::from_str(&error.to_string()))
5600 }
5601
5602 #[deprecated(note = "use submit_browser_desired_peers_with_sparse_fanout for sparse rooms")]
5604 pub fn submit_browser_desired_peers(
5605 &self,
5606 revision: u32,
5607 peers_json: String,
5608 ) -> Result<bool, JsValue> {
5609 self.inner
5610 .submit_browser_desired_peers(u64::from(revision), &peers_json)
5611 .map_err(|error| JsValue::from_str(&error.to_string()))
5612 }
5613
5614 pub async fn submit_browser_desired_peers_with_sparse_fanout(
5615 &self,
5616 revision: u32,
5617 peers_json: String,
5618 capability: Option<String>,
5619 local_device_id: Option<String>,
5620 local_device_key_x: Option<String>,
5621 sparse: Option<bool>,
5622 ) -> Result<bool, JsValue> {
5623 let peers_json = if sparse.unwrap_or(false) {
5624 let capability = capability
5625 .as_deref()
5626 .ok_or_else(|| JsValue::from_str("sparse fanout requires a capability"))?;
5627 let local_device_id = local_device_id
5628 .as_deref()
5629 .ok_or_else(|| JsValue::from_str("sparse fanout requires a local device id"))?;
5630 let local_device_key_x = local_device_key_x.as_deref().ok_or_else(|| {
5631 JsValue::from_str("sparse fanout requires a local device key")
5632 })?;
5633 self.inner
5634 .configure_sparse_fanout(
5635 capability,
5636 u64::from(revision),
5637 local_device_id,
5638 local_device_key_x,
5639 &peers_json,
5640 true,
5641 )
5642 .await
5643 .map_err(|error| JsValue::from_str(&error.to_string()))?
5644 .0
5645 } else {
5646 peers_json
5647 };
5648 self.inner
5649 .submit_browser_desired_peers(u64::from(revision), &peers_json)
5650 .map_err(|error| JsValue::from_str(&error.to_string()))
5651 }
5652
5653 #[wasm_bindgen(js_name = prepareSparseFanoutMessage)]
5654 pub async fn prepare_sparse_fanout_message(
5655 &self,
5656 capability: String,
5657 payload: Vec<u8>,
5658 ) -> Result<JsValue, JsValue> {
5659 let request = self
5660 .inner
5661 .prepare_sparse_fanout_message(&capability, &payload)
5662 .await
5663 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5664 serde_wasm_bindgen::to_value(&request)
5665 .map_err(|error| JsValue::from_str(&error.to_string()))
5666 }
5667
5668 #[wasm_bindgen(js_name = finalizeSparseFanoutMessage)]
5669 pub async fn finalize_sparse_fanout_message(
5670 &self,
5671 request_id: String,
5672 signature: String,
5673 ) -> Result<Vec<u8>, JsValue> {
5674 self.inner
5675 .finalize_sparse_fanout_message(&request_id, &signature)
5676 .await
5677 .map_err(|error| JsValue::from_str(&error.to_string()))
5678 }
5679
5680 #[wasm_bindgen(js_name = acceptSparseFanoutMessage)]
5681 pub async fn accept_sparse_fanout_message(
5682 &self,
5683 capability: String,
5684 source_peer_id: String,
5685 encoded: Vec<u8>,
5686 ) -> Result<JsValue, JsValue> {
5687 let decision = self
5688 .inner
5689 .accept_sparse_fanout_message(&capability, &source_peer_id, &encoded)
5690 .await
5691 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5692 serde_wasm_bindgen::to_value(&decision)
5693 .map_err(|error| JsValue::from_str(&error.to_string()))
5694 }
5695
5696 #[wasm_bindgen(js_name = sparseFanoutDiagnostics)]
5697 pub async fn sparse_fanout_diagnostics(
5698 &self,
5699 capability: String,
5700 ) -> Result<JsValue, JsValue> {
5701 serde_wasm_bindgen::to_value(&self.inner.sparse_fanout_diagnostics(&capability).await)
5702 .map_err(|error| JsValue::from_str(&error.to_string()))
5703 }
5704
5705 #[wasm_bindgen(js_name = recordSparseFanoutForwardQueueDrop)]
5706 pub async fn record_sparse_fanout_forward_queue_drop(
5707 &self,
5708 capability: String,
5709 count: u32,
5710 ) -> Result<(), JsValue> {
5711 self.inner
5712 .record_sparse_fanout_forward_queue_drop(&capability, u64::from(count))
5713 .await
5714 .map_err(|error| JsValue::from_str(&error.to_string()))
5715 }
5716
5717 pub fn wake_browser_auto_connect(&self) -> bool {
5718 self.inner.wake_browser_auto_connect()
5719 }
5720
5721 pub async fn set_auto_connect_excluded(&self, device_id: String, excluded: bool) {
5722 if excluded {
5723 self.inner.exclude_peer_and_publish(&device_id).await;
5724 } else {
5725 self.inner.unexclude_peer_and_publish(&device_id).await;
5726 }
5727 self.inner.wake_browser_auto_connect();
5728 }
5729
5730 pub fn is_auto_connect_excluded(&self, device_id: String) -> bool {
5731 self.inner.is_auto_connect_excluded(&device_id)
5732 }
5733
5734 pub async fn disconnect_device(
5735 &self,
5736 device_id: String,
5737 node_id_hint: Option<String>,
5738 ) -> Result<JsValue, JsValue> {
5739 let retired = self
5740 .inner
5741 .disconnect_device(&device_id, node_id_hint.as_deref())
5742 .await;
5743 serde_wasm_bindgen::to_value(&retired).map_err(|e| JsValue::from_str(&e.to_string()))
5744 }
5745
5746 pub fn stop_auth_scoped_activity(&self) {
5747 self.inner.stop_auth_scoped_activity();
5748 self.inner.stop_browser_auto_connect();
5749 }
5750
5751 pub fn start_presence_loop(
5752 &self,
5753 user_id: String,
5754 device_name: String,
5755 ticket: String,
5756 metadata: Option<String>,
5757 ) {
5758 self.inner
5759 .clone()
5760 .start_signaling_loop(user_id, device_name, ticket, metadata);
5761 }
5762
5763 pub async fn search_devices(&self, user_id: String) -> Result<JsValue, JsValue> {
5764 let devices = self
5765 .inner
5766 .search_devices(&user_id)
5767 .await
5768 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5769 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
5770 }
5771
5772 pub async fn devices_with_status(&self, user_id: String) -> Result<JsValue, JsValue> {
5773 let devices = self
5774 .inner
5775 .devices_with_status(&user_id)
5776 .await
5777 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5778 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
5779 }
5780
5781 pub async fn connect_device(
5782 &self,
5783 device_id: Option<String>,
5784 endpoint_ticket: String,
5785 ) -> Result<JsValue, JsValue> {
5786 let result = self
5787 .inner
5788 .connect_device(device_id.as_deref(), &endpoint_ticket)
5789 .await
5790 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5791 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
5792 }
5793
5794 pub async fn create_session(&self, session_json: String) -> Result<(), JsValue> {
5795 let session: crate::signaling::SignalingSession =
5796 serde_json::from_str(&session_json)
5797 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5798 self.inner
5799 .create_session(session)
5800 .await
5801 .map_err(|e| JsValue::from_str(&e.to_string()))
5802 }
5803
5804 pub async fn update_session(
5805 &self,
5806 session_id: String,
5807 update_json: String,
5808 ) -> Result<(), JsValue> {
5809 let update_data: serde_json::Value = serde_json::from_str(&update_json)
5810 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5811 self.inner
5812 .update_session(&session_id, update_data)
5813 .await
5814 .map_err(|e| JsValue::from_str(&e.to_string()))
5815 }
5816
5817 pub async fn create_room(
5820 &self,
5821 room_id: String,
5822 user_id: String,
5823 ticket_str: String,
5824 my_node_id: String,
5825 tag: String,
5826 max_members: Option<u32>,
5827 ) -> Result<bool, JsValue> {
5828 self.inner
5829 .room
5830 .create_room(
5831 &room_id,
5832 &user_id,
5833 &ticket_str,
5834 &my_node_id,
5835 &tag,
5836 max_members,
5837 )
5838 .await
5839 .map_err(|e| JsValue::from_str(&e.to_string()))
5840 }
5841
5842 pub async fn join_room(
5843 &self,
5844 room_id: String,
5845 user_id: String,
5846 ticket_str: String,
5847 my_node_id: String,
5848 tag: String,
5849 ) -> Result<(), JsValue> {
5850 self.inner
5851 .room
5852 .join_room(&room_id, &user_id, &ticket_str, &my_node_id, &tag)
5853 .await
5854 .map_err(|e| JsValue::from_str(&e.to_string()))
5855 }
5856
5857 pub async fn get_members(
5858 &self,
5859 room_id: String,
5860 my_node_id: String,
5861 tag: String,
5862 ) -> Result<String, JsValue> {
5863 let members = self
5864 .inner
5865 .room
5866 .get_members(&room_id, &my_node_id, &tag)
5867 .await
5868 .map_err(|e| JsValue::from_str(&e.to_string()))?;
5869 serde_json::to_string(&members).map_err(|e| JsValue::from_str(&e.to_string()))
5870 }
5871
5872 pub async fn leave_room(
5873 &self,
5874 room_id: String,
5875 my_node_id: String,
5876 tag: String,
5877 ) -> Result<(), JsValue> {
5878 self.inner
5879 .room
5880 .leave_room(&room_id, &my_node_id, &tag)
5881 .await
5882 .map_err(|e| JsValue::from_str(&e.to_string()))
5883 }
5884 }
5885
5886 #[cfg(feature = "iroh-protocols-wasm")]
5887 #[wasm_bindgen]
5888 impl WasmClient {
5889 #[wasm_bindgen(js_name = __initPersistentIrohProtocols)]
5893 pub async fn init_persistent_iroh_protocols(
5894 &self,
5895 replica_store: JsValue,
5896 ) -> Result<(), JsValue> {
5897 let mut guard = self.persistent_protocols.lock().await;
5898 if guard.is_some() {
5899 return Ok(());
5900 }
5901 let node = self
5902 .inner
5903 .iroh_node
5904 .read()
5905 .await
5906 .as_ref()
5907 .cloned()
5908 .ok_or_else(|| {
5909 JsValue::from_str("OpenRTC endpoint must be initialized before protocols")
5910 })?;
5911 let store = crate::wasm_docs_persistence::JsReplicaStore::new(replica_store)
5912 .map_err(|error| JsValue::from_str(&error.to_string()))?;
5913 let actor = crate::wasm_docs_persistence::WasmPersistentDocsActor::hydrate(
5914 store,
5915 node.endpoint().clone(),
5916 )
5917 .await
5918 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
5919 node.install_standard_protocols(
5920 actor.docs_protocol(),
5921 actor.blobs_protocol(),
5922 actor.gossip_protocol(),
5923 )
5924 .await
5925 .map_err(|error| JsValue::from_str(&format!("{error:#}")))?;
5926 *guard = Some(actor);
5927 Ok(())
5928 }
5929
5930 #[wasm_bindgen(js_name = __reconcilePersistentIrohProtocols)]
5934 pub async fn reconcile_persistent_iroh_protocols(&self) -> Result<u32, JsValue> {
5935 let guard = self.persistent_protocols.lock().await;
5936 let Some(actor) = guard.as_ref() else {
5937 return Ok(0);
5938 };
5939 actor.reconcile_sync().await.map_err(js_protocol_error)
5940 }
5941
5942 #[wasm_bindgen(js_name = __importPersistentIrohAuthor)]
5943 pub async fn import_persistent_iroh_author(
5944 &self,
5945 author_secret: Vec<u8>,
5946 ) -> Result<String, JsValue> {
5947 let guard = self.persistent_protocols.lock().await;
5948 let actor = persistent_protocols(&guard)?;
5949 actor
5950 .import_author(author_secret)
5951 .await
5952 .map_err(js_protocol_error)
5953 }
5954
5955 #[wasm_bindgen(js_name = __importPersistentIrohNamespace)]
5956 pub async fn import_persistent_iroh_namespace(
5957 &self,
5958 capability_kind: String,
5959 capability: Vec<u8>,
5960 generation: u64,
5961 share_revision: u64,
5962 ) -> Result<String, JsValue> {
5963 let mut guard = self.persistent_protocols.lock().await;
5964 let actor = persistent_protocols_mut(&mut guard)?;
5965 actor
5966 .import_namespace(
5967 capability_kind.as_str(),
5968 capability,
5969 generation,
5970 share_revision,
5971 )
5972 .await
5973 .map_err(js_protocol_error)
5974 }
5975
5976 #[wasm_bindgen(js_name = __createPersistentIrohNamespace)]
5977 pub async fn create_persistent_iroh_namespace(
5978 &self,
5979 generation: u64,
5980 share_revision: u64,
5981 ) -> Result<JsValue, JsValue> {
5982 let mut guard = self.persistent_protocols.lock().await;
5983 let descriptor = persistent_protocols_mut(&mut guard)?
5984 .create_namespace(generation, share_revision)
5985 .await
5986 .map_err(js_protocol_error)?;
5987 serde_wasm_bindgen::to_value(&descriptor)
5988 .map_err(|error| JsValue::from_str(&error.to_string()))
5989 }
5990
5991 #[wasm_bindgen(js_name = __importPersistentIrohTicket)]
5992 pub async fn import_persistent_iroh_ticket(
5993 &self,
5994 ticket: String,
5995 generation: u64,
5996 share_revision: u64,
5997 ) -> Result<String, JsValue> {
5998 let mut guard = self.persistent_protocols.lock().await;
5999 persistent_protocols_mut(&mut guard)?
6000 .import_ticket(&ticket, generation, share_revision)
6001 .await
6002 .map_err(js_protocol_error)
6003 }
6004
6005 #[wasm_bindgen(js_name = __sharePersistentIrohNamespace)]
6006 pub async fn share_persistent_iroh_namespace(
6007 &self,
6008 namespace_id: String,
6009 writable: bool,
6010 ) -> Result<String, JsValue> {
6011 let guard = self.persistent_protocols.lock().await;
6012 persistent_protocols(&guard)?
6013 .share(&namespace_id, writable)
6014 .await
6015 .map_err(js_protocol_error)
6016 }
6017
6018 #[wasm_bindgen(js_name = __removePersistentIrohNamespace)]
6019 pub async fn remove_persistent_iroh_namespace(
6020 &self,
6021 namespace_id: String,
6022 generation: u64,
6023 share_revision: u64,
6024 ) -> Result<(), JsValue> {
6025 let mut guard = self.persistent_protocols.lock().await;
6026 persistent_protocols_mut(&mut guard)?
6027 .remove_namespace(&namespace_id, generation, share_revision)
6028 .await
6029 .map_err(js_protocol_error)
6030 }
6031
6032 #[wasm_bindgen(js_name = __putPersistentIrohBytes)]
6033 pub async fn put_persistent_iroh_bytes(
6034 &self,
6035 namespace_id: String,
6036 key: Vec<u8>,
6037 value: Vec<u8>,
6038 ) -> Result<JsValue, JsValue> {
6039 let guard = self.persistent_protocols.lock().await;
6040 let receipt = persistent_protocols(&guard)?
6041 .set_bytes(&namespace_id, key, value)
6042 .await
6043 .map_err(js_protocol_error)?;
6044 serde_wasm_bindgen::to_value(&receipt)
6045 .map_err(|error| JsValue::from_str(&error.to_string()))
6046 }
6047
6048 #[wasm_bindgen(js_name = __setPersistentIrohHash)]
6049 pub async fn set_persistent_iroh_hash(
6050 &self,
6051 namespace_id: String,
6052 key: Vec<u8>,
6053 content_hash: String,
6054 content_length: u64,
6055 ) -> Result<String, JsValue> {
6056 let guard = self.persistent_protocols.lock().await;
6057 persistent_protocols(&guard)?
6058 .set_hash(&namespace_id, key, &content_hash, content_length)
6059 .await
6060 .map_err(js_protocol_error)
6061 }
6062
6063 #[wasm_bindgen(js_name = __deletePersistentIrohPrefix)]
6064 pub async fn delete_persistent_iroh_prefix(
6065 &self,
6066 namespace_id: String,
6067 prefix: Vec<u8>,
6068 ) -> Result<JsValue, JsValue> {
6069 let guard = self.persistent_protocols.lock().await;
6070 let receipt = persistent_protocols(&guard)?
6071 .delete_prefix(&namespace_id, prefix)
6072 .await
6073 .map_err(js_protocol_error)?;
6074 serde_wasm_bindgen::to_value(&receipt)
6075 .map_err(|error| JsValue::from_str(&error.to_string()))
6076 }
6077
6078 #[wasm_bindgen(js_name = __queryPersistentIrohNamespace)]
6079 pub async fn query_persistent_iroh_namespace(
6080 &self,
6081 namespace_id: String,
6082 key_prefix: Vec<u8>,
6083 ) -> Result<JsValue, JsValue> {
6084 let guard = self.persistent_protocols.lock().await;
6085 let entries = persistent_protocols(&guard)?
6086 .query(&namespace_id, key_prefix)
6087 .await
6088 .map_err(js_protocol_error)?;
6089 serde_wasm_bindgen::to_value(&entries)
6090 .map_err(|error| JsValue::from_str(&error.to_string()))
6091 }
6092
6093 #[wasm_bindgen(js_name = __hydratePersistentIrohBlob)]
6094 pub async fn hydrate_persistent_iroh_blob(
6095 &self,
6096 content_hash: String,
6097 ) -> Result<(), JsValue> {
6098 let guard = self.persistent_protocols.lock().await;
6099 persistent_protocols(&guard)?
6100 .hydrate_blob(&content_hash)
6101 .await
6102 .map_err(js_protocol_error)
6103 }
6104
6105 #[wasm_bindgen(js_name = __acknowledgePersistentIrohOutbox)]
6106 pub async fn acknowledge_persistent_iroh_outbox(
6107 &self,
6108 operation_id: String,
6109 ) -> Result<(), JsValue> {
6110 let guard = self.persistent_protocols.lock().await;
6111 persistent_protocols(&guard)?
6112 .acknowledge_outbox(&operation_id)
6113 .await
6114 .map_err(js_protocol_error)
6115 }
6116
6117 #[wasm_bindgen(js_name = __flushPersistentIrohProtocols)]
6118 pub async fn flush_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
6119 let guard = self.persistent_protocols.lock().await;
6120 persistent_protocols(&guard)?
6121 .flush()
6122 .await
6123 .map_err(js_protocol_error)
6124 }
6125
6126 #[wasm_bindgen(js_name = __shutdownPersistentIrohProtocols)]
6127 pub async fn shutdown_persistent_iroh_protocols(&self) -> Result<(), JsValue> {
6128 if let Some(actor) = self.persistent_protocols.lock().await.take() {
6129 actor.shutdown().await.map_err(js_protocol_error)?;
6130 }
6131 Ok(())
6132 }
6133 }
6134
6135 #[cfg(feature = "iroh-protocols-wasm")]
6136 fn persistent_protocols(
6137 guard: &Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
6138 ) -> Result<&crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
6139 guard
6140 .as_ref()
6141 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
6142 }
6143
6144 #[cfg(feature = "iroh-protocols-wasm")]
6145 fn persistent_protocols_mut(
6146 guard: &mut Option<crate::wasm_docs_persistence::WasmPersistentDocsActor>,
6147 ) -> Result<&mut crate::wasm_docs_persistence::WasmPersistentDocsActor, JsValue> {
6148 guard
6149 .as_mut()
6150 .ok_or_else(|| JsValue::from_str("persistent Iroh protocols are not initialized"))
6151 }
6152
6153 #[cfg(feature = "iroh-protocols-wasm")]
6154 fn js_protocol_error(error: impl std::fmt::Display) -> JsValue {
6155 JsValue::from_str(&format!("{error:#}"))
6156 }
6157
6158 #[wasm_bindgen(start)]
6159 pub fn start() {
6160 console_error_panic_hook::set_once();
6161 }
6162}