1#![allow(deprecated)]
2
3pub mod application_crypto;
4pub mod application_crypto_streams;
5pub mod client;
6pub mod connection;
7pub mod coordination;
8pub mod explicit_transfer_crypto;
9pub(crate) mod generated;
10pub mod heartbeat;
11pub(crate) mod iroh_connection_policy;
12pub mod key_agreement;
13pub mod lifecycle_reason;
14pub(crate) mod native_moq_policy;
15pub mod native_protocol;
16#[cfg(not(target_arch = "wasm32"))]
17pub(crate) mod native_send_policy;
18pub(crate) mod native_webrtc_policy;
19pub mod presence;
20pub(crate) mod presence_policy;
21pub mod route_policy;
22pub mod runtime_policy;
23pub mod session_token;
24pub mod signaling;
25pub mod stream_metadata;
26pub(crate) mod transport_generation;
27pub(crate) mod transport_label;
28
29#[cfg(all(not(target_arch = "wasm32"), feature = "transport-lan"))]
30pub mod local_discovery;
31
32#[cfg(all(target_arch = "wasm32", feature = "transport-webrtc"))]
33compile_error!(
34 "feature `transport-webrtc` is native-only and must not be enabled for wasm32 targets"
35);
36
37#[cfg(all(target_arch = "wasm32", feature = "transport-moq"))]
38compile_error!("feature `transport-moq` is native-only and must not be enabled for wasm32 targets");
39
40#[cfg(test)]
44pub mod test_constants {
45 pub const TEST_PROJECT_ID: &str = "test-project";
46 pub const TEST_API_KEY: &str = "pk_test_0000000000000000000000000000000000000000";
47}
48
49pub const LIVE_PROJECT_ID: &str = "pluto-rtc-prod";
51
52pub fn validate_v2_public_api_key(api_key: &str) -> anyhow::Result<&str> {
56 let trimmed = api_key.trim();
57 let valid_prefix = trimmed.starts_with("pk_live_") || trimmed.starts_with("pk_test_");
58 let suffix = trimmed.get(8..).unwrap_or_default();
59 if !valid_prefix || suffix.len() != 40 || !suffix.bytes().all(|byte| byte.is_ascii_hexdigit()) {
60 anyhow::bail!("OpenRTC 2.0 requires a public pk_live_ or pk_test_ API key");
61 }
62 Ok(trimmed)
63}
64
65pub fn app_tag_from_api_key(api_key: &str) -> String {
66 let trimmed = api_key.trim();
67 if trimmed.is_empty() {
68 return "app_anonymous".to_string();
69 }
70
71 let suffix_len = trimmed.len().min(16);
72 format!("app_{}", &trimmed[trimmed.len() - suffix_len..])
73}
74
75pub fn space_app_tag_from_keys(api_key: &str, space_key: &str) -> String {
76 let input = format!("{}:{}", api_key.trim(), space_key.trim());
77 let digest = <sha2::Sha256 as sha2::Digest>::digest(input.as_bytes());
78 format!("space::{}", hex::encode(digest))
79}
80
81#[cfg(test)]
82mod v2_constructor_contract_tests {
83 use super::*;
84
85 #[test]
86 fn rust_v2_constructor_is_provider_neutral_and_side_effect_free() {
87 let api_key = test_constants::TEST_API_KEY;
88 let client = client::Client::new_v2(api_key.to_string()).expect("valid public API key");
89
90 assert_eq!(client.app_tag(), app_tag_from_api_key(api_key));
91 assert!(client::Client::new_v2("firebase-project-id".to_string()).is_err());
92 }
93}
94
95#[cfg(not(target_arch = "wasm32"))]
96pub fn ensure_default_rustls_provider() {
97 if rustls::crypto::CryptoProvider::get_default().is_none() {
98 let _ = rustls::crypto::ring::default_provider().install_default();
99 }
100}
101
102#[cfg(not(target_arch = "wasm32"))]
103pub mod adapters;
104
105#[cfg(not(target_arch = "wasm32"))]
106pub mod native_coordination_gateway;
107
108#[cfg(not(target_arch = "wasm32"))]
109pub mod native_v2;
110
111pub mod connection_manager;
112pub mod logging;
113
114#[cfg(not(target_arch = "wasm32"))]
115pub mod protocol_registry;
116
117#[cfg(not(target_arch = "wasm32"))]
118pub mod runtime_manager;
119
120#[cfg(not(target_arch = "wasm32"))]
121pub mod transport;
122
123#[cfg(not(target_arch = "wasm32"))]
124pub use client::EndpointHandle;
125
126#[cfg(all(
127 not(target_arch = "wasm32"),
128 not(any(target_os = "ios", target_os = "android"))
129))]
130pub mod sso;
131
132#[cfg(not(target_arch = "wasm32"))]
133pub mod native_node;
134
135#[cfg(not(target_arch = "wasm32"))]
136pub mod native_device;
137
138#[cfg(not(target_arch = "wasm32"))]
139pub mod native_auth;
140
141#[cfg(target_arch = "wasm32")]
142pub mod wasm_node;
143
144#[cfg(target_arch = "wasm32")]
145#[macro_export]
146macro_rules! console_log {
147 ($($t:tt)*) => (web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format_args!($($t)*).to_string())))
148}
149
150#[cfg(target_arch = "wasm32")]
152pub mod wasm_api {
153 use crate::client::Client;
154 use crate::session_token::split_compound_ticket;
155 use crate::wasm_node::{
156 into_js_readable_stream, peer_uni_stream_from_send, BiStream, PeerUniStream,
157 };
158 use iroh_tickets::endpoint::EndpointTicket;
159 use std::str::FromStr;
160 use std::sync::{Arc, Mutex};
161 use wasm_bindgen::prelude::*;
162 use wasm_streams::readable::sys::ReadableStream as JsReadableStream;
163
164 #[wasm_bindgen]
165 pub struct WasmClient {
166 inner: Arc<Client>,
167 identity_credential: Arc<Mutex<Option<String>>>,
168 last_auth_log: Arc<Mutex<Option<(bool, usize)>>>,
169 }
170
171 #[wasm_bindgen]
172 impl WasmClient {
173 #[cfg(feature = "legacy-v1")]
174 #[deprecated(
175 since = "2.0.0",
176 note = "rollback-only: use WasmClient.newV2(apiKey) through the openrtc/runtime adapter"
177 )]
178 #[wasm_bindgen(constructor)]
179 pub fn new(project_id: String, tag: String) -> Result<WasmClient, JsValue> {
180 Self::new_with_app_tag(project_id, tag)
181 }
182
183 #[cfg(feature = "legacy-v1")]
184 #[deprecated(
185 since = "2.0.0",
186 note = "rollback-only: use WasmClient.newV2(apiKey) through the openrtc/runtime adapter"
187 )]
188 #[wasm_bindgen(js_name = newWithAppTag)]
189 pub fn new_with_app_tag(
190 project_id: String,
191 app_tag: String,
192 ) -> Result<WasmClient, JsValue> {
193 let identity_credential: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
194 let token_state = identity_credential.clone();
195 let token_provider = Box::new(move || -> Option<String> {
196 token_state.lock().ok().and_then(|guard| guard.clone())
197 });
198
199 Ok(Self {
200 inner: Arc::new(Client::new_with_app_tag(
201 project_id,
202 app_tag,
203 token_provider,
204 )),
205 identity_credential,
206 last_auth_log: Arc::new(Mutex::new(None)),
207 })
208 }
209
210 #[wasm_bindgen(js_name = newV2)]
214 pub fn new_v2(api_key: String) -> Result<WasmClient, JsValue> {
215 let api_key = crate::validate_v2_public_api_key(&api_key)
216 .map_err(|error| JsValue::from_str(&error.to_string()))?;
217 let app_tag = crate::app_tag_from_api_key(api_key);
218 let identity_credential: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
219 let credential_state = identity_credential.clone();
220 let identity_credential_provider = Box::new(move || -> Option<String> {
221 credential_state.lock().ok().and_then(|guard| guard.clone())
222 });
223
224 Ok(Self {
225 inner: Arc::new(Client::new_provider_neutral(
226 app_tag,
227 identity_credential_provider,
228 )),
229 identity_credential,
230 last_auth_log: Arc::new(Mutex::new(None)),
231 })
232 }
233
234 #[wasm_bindgen(js_name = setIdentityCredential)]
235 pub fn set_identity_credential(&self, credential: Option<String>) {
236 let has_credential = credential
237 .as_ref()
238 .map(|value| !value.is_empty())
239 .unwrap_or(false);
240 let credential_len = credential.as_ref().map(|value| value.len()).unwrap_or(0);
241
242 if let Ok(mut guard) = self.identity_credential.lock() {
243 *guard = credential.filter(|value| !value.is_empty());
244 }
245
246 let should_log = if let Ok(mut guard) = self.last_auth_log.lock() {
247 let next = (has_credential, credential_len);
248 if guard.as_ref() == Some(&next) {
249 false
250 } else {
251 *guard = Some(next);
252 true
253 }
254 } else {
255 true
256 };
257
258 if should_log {
259 web_sys::console::log_1(&JsValue::from_str(&format!(
260 "[OPENRTC][WASM-IDENTITY] credential updated present={} len={}",
261 has_credential, credential_len
262 )));
263 }
264 }
265
266 #[wasm_bindgen(js_name = clearIdentityCredential)]
272 pub fn clear_identity_credential(&self) {
273 self.set_identity_credential(None);
274 }
275
276 #[cfg(feature = "legacy-v1")]
278 #[deprecated(since = "2.0.0", note = "rollback-only: use setIdentityCredential")]
279 pub fn set_auth_token(&self, token: Option<String>) {
280 self.set_identity_credential(token);
281 }
282
283 #[wasm_bindgen(js_name = rankRoutes)]
286 pub fn rank_routes(
287 &self,
288 configured_priority: Vec<String>,
289 candidates: Vec<String>,
290 ) -> Vec<String> {
291 crate::route_policy::rank_routes(&configured_priority, &candidates)
292 }
293
294 pub async fn init_iroh(&self, secret_key: Option<Vec<u8>>) -> Result<String, JsValue> {
295 let started_at = js_sys::Date::now();
296 web_sys::console::log_1(&JsValue::from_str(&format!(
297 "[OPENRTC][WASM-API] init_iroh called has_secret_key={} secret_key_len={}",
298 secret_key.as_ref().is_some(),
299 secret_key.as_ref().map(|k| k.len()).unwrap_or(0)
300 )));
301 match self.inner.init_iroh(secret_key, vec![]).await {
302 Ok(node_id) => {
303 self.inner.clone().start_wasm_accept_bridge();
304 let elapsed = js_sys::Date::now() - started_at;
305 web_sys::console::log_1(&JsValue::from_str(&format!(
306 "[OPENRTC][WASM-API] init_iroh success elapsed_ms={:.0} node_id={}",
307 elapsed, node_id
308 )));
309 Ok(node_id)
310 }
311 Err(err) => {
312 let elapsed = js_sys::Date::now() - started_at;
313 web_sys::console::error_1(&JsValue::from_str(&format!(
314 "[OPENRTC][WASM-API] init_iroh failed elapsed_ms={:.0} error={}",
315 elapsed, err
316 )));
317 Err(JsValue::from_str(&err.to_string()))
318 }
319 }
320 }
321
322 #[wasm_bindgen(js_name = initIrohWithTestRelay)]
325 pub async fn init_iroh_with_test_relay(
326 &self,
327 secret_key: Option<Vec<u8>>,
328 test_relay_url: Option<String>,
329 ) -> Result<String, JsValue> {
330 let node_id = self
331 .inner
332 .init_iroh_with_test_relay(secret_key, vec![], test_relay_url.as_deref())
333 .await
334 .map_err(|error| JsValue::from_str(&error.to_string()))?;
335 self.inner.clone().start_wasm_accept_bridge();
339 Ok(node_id)
340 }
341
342 pub async fn iroh_secret_key(&self) -> Result<Vec<u8>, JsValue> {
343 let node_guard = self.inner.iroh_node.read().await;
344 if let Some(node) = node_guard.as_ref() {
345 Ok(node.secret_key())
346 } else {
347 Err(JsValue::from_str("Iroh node not initialized"))
348 }
349 }
350
351 pub async fn node_addr(&self) -> Result<String, JsValue> {
352 let node_guard = self.inner.iroh_node.read().await;
353 if let Some(node) = node_guard.as_ref() {
354 let addr = node
355 .node_addr()
356 .await
357 .map_err(|e| JsValue::from_str(&e.to_string()))?;
358 serde_json::to_string(&addr).map_err(|e| JsValue::from_str(&e.to_string()))
359 } else {
360 Err(JsValue::from_str("Iroh node not initialized"))
361 }
362 }
363
364 pub async fn endpoint_ticket(&self) -> Result<String, JsValue> {
365 self.inner
366 .endpoint_ticket()
367 .await
368 .map_err(|e| JsValue::from_str(&e.to_string()))
369 }
370
371 pub async fn endpoint_ticket_with_token(
375 &self,
376 grant_scope: String,
377 max_connections: u32,
378 ) -> Result<String, JsValue> {
379 self.inner
380 .endpoint_ticket_with_token(&grant_scope, max_connections)
381 .await
382 .map_err(|e| JsValue::from_str(&e.to_string()))
383 }
384
385 pub fn register_session_token(
387 &self,
388 token: String,
389 grant_scope: String,
390 max_connections: u32,
391 ) {
392 self.inner
393 .register_session_token(token, grant_scope, max_connections);
394 }
395
396 pub fn register_session_token_with_expiry_ms(
398 &self,
399 token: String,
400 grant_scope: String,
401 max_connections: u32,
402 expires_at_ms: u64,
403 ) {
404 self.inner.register_session_token_with_expiry_ms(
405 token,
406 grant_scope,
407 max_connections,
408 expires_at_ms,
409 );
410 }
411
412 #[wasm_bindgen(js_name = setConnectionApplicationCryptoRequired)]
414 pub fn set_connection_application_crypto_required(
415 &self,
416 connection_id: String,
417 ) -> Result<(), JsValue> {
418 self.inner
419 .set_connection_application_crypto_required(&connection_id);
420 Ok(())
421 }
422
423 #[wasm_bindgen(js_name = setConnectionApplicationCryptoKey)]
425 pub async fn set_connection_application_crypto_key(
426 &self,
427 connection_id: String,
428 key: Vec<u8>,
429 ) -> Result<(), JsValue> {
430 if key.len() != crate::application_crypto::APPLICATION_KEY_BYTES {
431 return Err(JsValue::from_str("application crypto key must be 32 bytes"));
432 }
433 let mut key_bytes = [0u8; crate::application_crypto::APPLICATION_KEY_BYTES];
434 key_bytes.copy_from_slice(&key);
435 self.inner
436 .set_connection_application_crypto_key(&connection_id, key_bytes);
437 self.inner
438 .emit_current_wasm_connection_state(&connection_id)
439 .await;
440 Ok(())
441 }
442
443 #[wasm_bindgen(js_name = clearConnectionApplicationCryptoKey)]
448 pub fn clear_connection_application_crypto_key(&self, connection_id: String) {
449 self.inner
450 .clear_connection_application_crypto_key(&connection_id);
451 }
452
453 pub fn validate_session_token(&self, token: String) -> Result<String, JsValue> {
457 self.inner
458 .validate_session_token(&token)
459 .map_err(|e| JsValue::from_str(&e))
460 }
461
462 pub async fn validate_session_token_for_connection(
464 &self,
465 token: String,
466 connection_id: String,
467 ) -> Result<String, JsValue> {
468 self.inner
469 .validate_session_token_for_connection(&token, &connection_id)
470 .await
471 .map_err(|e| JsValue::from_str(&e))
472 }
473
474 pub async fn validate_session_token_for_connection_with_payload(
475 &self,
476 token: String,
477 connection_id: String,
478 token_payload: Option<String>,
479 ) -> Result<String, JsValue> {
480 self.inner
481 .validate_session_token_for_connection_with_payload(
482 &token,
483 &connection_id,
484 token_payload.as_deref(),
485 )
486 .await
487 .map_err(|e| JsValue::from_str(&e))
488 }
489
490 pub async fn present_session_token_to_host(
497 &self,
498 endpoint_id: String,
499 token: String,
500 ) -> Result<String, JsValue> {
501 self.present_session_token_to_host_with_payload(endpoint_id, token, None)
502 .await
503 }
504
505 pub async fn present_session_token_to_host_with_payload(
506 &self,
507 endpoint_id: String,
508 token: String,
509 token_payload: Option<String>,
510 ) -> Result<String, JsValue> {
511 self.present_session_token_to_host_with_payload_and_device_id(
512 endpoint_id,
513 token,
514 token_payload,
515 None,
516 )
517 .await
518 }
519
520 pub async fn present_session_token_to_host_with_payload_and_device_id(
521 &self,
522 endpoint_id: String,
523 token: String,
524 token_payload: Option<String>,
525 device_id: Option<String>,
526 ) -> Result<String, JsValue> {
527 crate::console_log!(
528 "[OpenRTC][session-admission][wasm-present] endpoint_id={} claimed_local_device_id={}",
529 endpoint_id,
530 device_id.as_deref().unwrap_or("<none>")
531 );
532 let endpoint_id_parsed: iroh::EndpointId = endpoint_id
533 .parse()
534 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
535 let local_node_id = self.inner.current_node_id().await.ok_or_else(|| {
536 JsValue::from_str("missing local node id for session-token presentation")
537 })?;
538 let connection_id =
539 crate::client::Client::deterministic_connection_id(&local_node_id, &endpoint_id);
540 let approval_scope = self
541 .inner
542 .present_and_accept_session_token_with_local_claim(
543 endpoint_id_parsed,
544 &connection_id,
545 &token,
546 token_payload.as_deref(),
547 None,
548 device_id,
549 )
550 .await
551 .map_err(|e| JsValue::from_str(&e))?;
552 self.inner
553 .emit_current_wasm_connection_state(&connection_id)
554 .await;
555
556 Ok(approval_scope)
557 }
558
559 pub async fn remote_session_admission_ready_for_ticket(
562 &self,
563 endpoint_ticket: String,
564 ) -> Result<bool, JsValue> {
565 self.inner
566 .remote_session_admission_ready_for_ticket(&endpoint_ticket)
567 .await
568 .map_err(|error| JsValue::from_str(&error.to_string()))
569 }
570
571 #[allow(clippy::too_many_arguments)]
574 pub async fn prepare_inline_reciprocal_session_admission(
575 &self,
576 endpoint_id: String,
577 expected_transport_stable_id: u64,
578 stream_instance_id: String,
579 presentation_id: String,
580 token: String,
581 token_payload: String,
582 device_id: String,
583 stream_contract: String,
584 ) -> Result<bool, JsValue> {
585 let endpoint_id: iroh::EndpointId = endpoint_id
586 .parse()
587 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
588 let stream_contract = match stream_contract.trim() {
589 "one-shot-admission" => {
590 crate::native_protocol::SessionTokenStreamContract::OneShotAdmission
591 }
592 "persistent-control" => {
593 crate::native_protocol::SessionTokenStreamContract::PersistentControl
594 }
595 other => {
596 return Err(JsValue::from_str(&format!(
597 "unsupported reciprocal stream contract: {other}"
598 )))
599 }
600 };
601 self.inner
602 .prepare_inline_reciprocal_session_admission(
603 endpoint_id,
604 expected_transport_stable_id,
605 stream_instance_id.as_str(),
606 presentation_id.as_str(),
607 token.as_str(),
608 token_payload.as_str(),
609 device_id.as_str(),
610 stream_contract,
611 )
612 .await
613 .map_err(|error| JsValue::from_str(&error))?;
614 Ok(true)
615 }
616
617 pub async fn confirm_inline_reciprocal_session_admission(
620 &self,
621 endpoint_id: String,
622 expected_transport_stable_id: u64,
623 stream_instance_id: String,
624 presentation_id: String,
625 accepted: bool,
626 approval_scope: Option<String>,
627 ) -> Result<bool, JsValue> {
628 let endpoint_id: iroh::EndpointId = endpoint_id
629 .parse()
630 .map_err(|error| JsValue::from_str(&format!("{error}")))?;
631 self.inner
632 .confirm_inline_reciprocal_session_admission(
633 endpoint_id,
634 expected_transport_stable_id,
635 stream_instance_id.as_str(),
636 presentation_id.as_str(),
637 accepted,
638 approval_scope.as_deref(),
639 )
640 .await
641 .map_err(|error| JsValue::from_str(&error))?;
642 self.inner
643 .emit_current_wasm_connection_state(
644 &crate::client::Client::deterministic_connection_id(
645 &self.inner.current_node_id().await.ok_or_else(|| {
646 JsValue::from_str(
647 "missing local node id after reciprocal admission ACK",
648 )
649 })?,
650 &endpoint_id.to_string(),
651 ),
652 )
653 .await;
654 Ok(true)
655 }
656
657 pub fn revoke_session_token(&self, token: String) -> Result<JsValue, JsValue> {
659 serde_wasm_bindgen::to_value(&self.inner.revoke_session_token(&token))
660 .map_err(|error| JsValue::from_str(&error.to_string()))
661 }
662
663 pub async fn revoke_tokens_by_scope(
665 &self,
666 grant_scope: String,
667 ) -> Result<JsValue, JsValue> {
668 let affected = self.inner.revoke_tokens_by_scope(&grant_scope).await;
669 serde_wasm_bindgen::to_value(&affected).map_err(|e| JsValue::from_str(&e.to_string()))
670 }
671
672 pub fn clear_session_tokens(&self) {
674 self.inner.clear_session_tokens();
675 }
676
677 pub fn endpoint_id_from_ticket(&self, ticket: String) -> Result<String, JsValue> {
678 let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
679 let parsed = EndpointTicket::from_str(iroh_ticket)
680 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
681 Ok(parsed.endpoint_addr().id.to_string())
682 }
683
684 pub async fn connect(&self, ticket: String) -> Result<JsReadableStream, JsValue> {
688 web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(
689 "[OPENRTC][WASM-API] WasmClient.connect() is deprecated; use connect_device() for managed product dials.",
690 ));
691 let (iroh_ticket, _token_suffix) = split_compound_ticket(ticket.trim());
692 let parsed = EndpointTicket::from_str(iroh_ticket)
693 .map_err(|e| JsValue::from_str(&format!("Invalid endpoint ticket: {}", e)))?;
694 let endpoint_addr = parsed.endpoint_addr().clone();
695 let endpoint_id = endpoint_addr.id;
696 let stream = {
697 let node_guard = self.inner.iroh_node.read().await;
698 if let Some(node) = node_guard.as_ref() {
699 node.connect_addr(endpoint_id, endpoint_addr)
700 } else {
701 return Err(JsValue::from_str("Iroh node not initialized"));
702 }
703 };
704 Ok(into_js_readable_stream(stream))
705 }
706
707 pub async fn disconnect(&self, endpoint_id: String) -> Result<(), JsValue> {
708 let endpoint_id: iroh::EndpointId = endpoint_id
709 .parse()
710 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
711 let node_guard = self.inner.iroh_node.read().await;
712 if let Some(node) = node_guard.as_ref() {
713 node.disconnect(endpoint_id)
714 .await
715 .map_err(|e| JsValue::from_str(&e.to_string()))
716 } else {
717 Err(JsValue::from_str("Iroh node not initialized"))
718 }
719 }
720
721 pub async fn disconnect_transient(&self, endpoint_id: String) -> Result<(), JsValue> {
734 let endpoint_id: iroh::EndpointId = endpoint_id
735 .parse()
736 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
737 self.inner
738 .disconnect_with_reason(
739 endpoint_id,
740 crate::lifecycle_reason::REASON_NETWORK_CHANGE_RECONNECT,
741 )
742 .await
743 .map_err(|e| JsValue::from_str(&e.to_string()))?;
744 self.inner.wake_browser_auto_connect();
750 Ok(())
751 }
752
753 pub async fn is_connected(&self, endpoint_id: String) -> Result<bool, JsValue> {
754 let endpoint_id: iroh::EndpointId = endpoint_id
755 .parse()
756 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
757 Ok(self.inner.is_connected(endpoint_id).await)
758 }
759
760 pub fn runtime_policy(&self) -> Result<JsValue, JsValue> {
761 serde_wasm_bindgen::to_value(&self.inner.runtime_policy_snapshot())
762 .map_err(|e| JsValue::from_str(&e.to_string()))
763 }
764
765 pub async fn add_peer_scope(&self, id: String, scope: String) -> Result<JsValue, JsValue> {
766 let scopes = self.inner.add_peer_scope(&id, &scope).await;
767 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
768 }
769
770 pub async fn release_peer_scope(
771 &self,
772 id: String,
773 scope: Option<String>,
774 ) -> Result<JsValue, JsValue> {
775 let scopes = self.inner.release_peer_scope(&id, scope.as_deref()).await;
776 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
777 }
778
779 pub async fn peer_scopes(&self, id: String) -> Result<JsValue, JsValue> {
780 let scopes = self.inner.peer_scopes(&id).await;
781 serde_wasm_bindgen::to_value(&scopes).map_err(|e| JsValue::from_str(&e.to_string()))
782 }
783
784 pub async fn same_peer(&self, left: String, right: String) -> Result<bool, JsValue> {
785 Ok(self.inner.same_peer(&left, &right).await)
786 }
787
788 pub async fn peer_snapshot(&self, id: String) -> Result<JsValue, JsValue> {
789 let snapshot = self.inner.peer_snapshot(&id).await;
790 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
791 }
792
793 pub async fn peer_session(&self, id: String) -> Result<JsValue, JsValue> {
797 let snapshot = self.inner.peer_session(&id).await;
798 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
799 }
800
801 pub async fn peer_sessions(&self) -> Result<JsValue, JsValue> {
802 let snapshots = self.inner.peer_sessions().await;
803 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
804 }
805
806 pub async fn connection_state(&self, connection_id: String) -> Result<JsValue, JsValue> {
807 let snapshot = self.inner.connection_state(&connection_id).await;
808 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
809 }
810
811 pub async fn connection_states(&self) -> Result<JsValue, JsValue> {
812 let snapshots = self.inner.connection_states().await;
813 serde_wasm_bindgen::to_value(&snapshots).map_err(|e| JsValue::from_str(&e.to_string()))
814 }
815
816 pub async fn wait_for_settled_peer(
817 &self,
818 id: String,
819 timeout_ms: Option<u32>,
820 ) -> Result<JsValue, JsValue> {
821 let snapshot = self
822 .inner
823 .wait_for_settled_peer(&id, timeout_ms.map(|value| value as u64))
824 .await;
825 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
826 }
827
828 pub async fn resolve_peer_connection_records(
829 &self,
830 id: String,
831 ) -> Result<JsValue, JsValue> {
832 let records = self.inner.resolve_peer_connection_records(&id).await;
833 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
834 }
835
836 pub async fn list_managed_connections(&self) -> Result<JsValue, JsValue> {
837 let records = self.inner.list_managed_connections().await;
838 serde_wasm_bindgen::to_value(&records).map_err(|e| JsValue::from_str(&e.to_string()))
839 }
840
841 pub async fn bind_connection_device_id(
842 &self,
843 connection_id: String,
844 device_id: String,
845 ) -> Result<JsValue, JsValue> {
846 let snapshot = self
847 .inner
848 .bind_connection_device_id(&connection_id, &device_id)
849 .await;
850 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
851 }
852
853 pub async fn bind_node_device_id(
854 &self,
855 node_id: String,
856 device_id: String,
857 ) -> Result<(), JsValue> {
858 self.inner.bind_node_device_id(&node_id, &device_id).await;
859 Ok(())
860 }
861
862 pub async fn reject_connection_admission(
863 &self,
864 connection_id: String,
865 reason: String,
866 ) -> Result<JsValue, JsValue> {
867 self.inner
868 .reject_session_connection(&connection_id, &reason);
869 self.inner
870 .emit_current_wasm_connection_state(&connection_id)
871 .await;
872 let snapshot = self.inner.connection_state(&connection_id).await;
873 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
874 }
875
876 pub async fn report_managed_connection_settled(
877 &self,
878 connection_id: String,
879 settled: bool,
880 device_id: Option<String>,
881 transport_stable_id: Option<u64>,
882 transport_generation: Option<u64>,
883 route_generation: Option<u64>,
884 ) -> Result<JsValue, JsValue> {
885 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
886 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
887 self.inner
888 .report_managed_connection_settled_for_transport(
889 &connection_id,
890 settled,
891 transport_stable_id,
892 transport_generation,
893 route_generation,
894 )
895 .await
896 }
897 _ => None,
898 };
899 let _ = device_id;
900 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
901 }
902
903 pub async fn report_transport_status(
904 &self,
905 connection_id: String,
906 active_transport: String,
907 parallel_transport: Option<String>,
908 transport_stable_id: Option<u64>,
909 transport_generation: Option<u64>,
910 route_generation: Option<u64>,
911 ) -> Result<JsValue, JsValue> {
912 let snapshot = match (transport_stable_id, transport_generation, route_generation) {
913 (Some(transport_stable_id), Some(transport_generation), Some(route_generation)) => {
914 self.inner
915 .report_transport_status_for_generation(
916 &connection_id,
917 &active_transport,
918 parallel_transport.as_deref(),
919 transport_stable_id,
920 transport_generation,
921 route_generation,
922 )
923 .await
924 }
925 _ => None,
926 };
927 serde_wasm_bindgen::to_value(&snapshot).map_err(|e| JsValue::from_str(&e.to_string()))
928 }
929
930 pub async fn is_current_transport_stable_id(
931 &self,
932 endpoint_id: String,
933 transport_stable_id: u64,
934 ) -> Result<bool, JsValue> {
935 let endpoint_id = endpoint_id
936 .parse::<iroh::EndpointId>()
937 .map_err(|error| JsValue::from_str(&error.to_string()))?;
938 Ok(self
939 .inner
940 .is_current_transport_stable_id(endpoint_id, transport_stable_id)
941 .await)
942 }
943
944 pub async fn open_bi(&self, endpoint_id: String) -> Result<BiStream, JsValue> {
945 let endpoint_id: iroh::EndpointId = endpoint_id
946 .parse()
947 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
948 self.inner
949 .assert_raw_peer_stream_allowed(&endpoint_id)
950 .await
951 .map_err(|e| JsValue::from_str(&e.to_string()))?;
952 self.inner
961 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
962 .await
963 .map_err(|e| JsValue::from_str(&e.to_string()))?;
964 let (send, recv) = self
965 .inner
966 .open_bi_internal(endpoint_id)
967 .await
968 .map_err(|e| JsValue::from_str(&e.to_string()))?;
969 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
970 }
971
972 pub async fn open_native_main_control_bi(
1001 &self,
1002 endpoint_id: String,
1003 ) -> Result<BiStream, JsValue> {
1004 let endpoint_id: iroh::EndpointId = endpoint_id
1005 .parse()
1006 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1007 self.inner
1008 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
1009 .await
1010 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1011 let (send, recv) = self
1012 .inner
1013 .open_bi_internal(endpoint_id)
1014 .await
1015 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1016 Ok(BiStream::from_parts(send, recv, endpoint_id.to_string()))
1017 }
1018
1019 pub async fn open_peer_bi(
1020 &self,
1021 id: String,
1022 timeout_ms: Option<u32>,
1023 ) -> Result<BiStream, JsValue> {
1024 let (_connection_id, remote_node_id, send, recv) = self
1025 .inner
1026 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
1027 .await
1028 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1029 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
1030 }
1031
1032 pub async fn send_peer_application_frame(
1036 &self,
1037 id: String,
1038 frame: Vec<u8>,
1039 timeout_ms: Option<u32>,
1040 ) -> Result<(), JsValue> {
1041 self.inner
1042 .send_peer_application_frame(&id, &frame, timeout_ms.map(|value| value as u64))
1043 .await
1044 .map_err(|error| JsValue::from_str(&error.to_string()))
1045 }
1046
1047 pub async fn open_peer_bi_explicit_file_sender(
1053 &self,
1054 id: String,
1055 timeout_ms: Option<u32>,
1056 ) -> Result<PeerUniStream, JsValue> {
1057 let (_connection_id, _remote_node_id, send) = self
1058 .inner
1059 .open_peer_bi_explicit_file_sender(&id, timeout_ms.map(|value| value as u64))
1060 .await
1061 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1062 Ok(peer_uni_stream_from_send(send))
1063 }
1064
1065 pub async fn open_peer_bi_transport_only(
1069 &self,
1070 id: String,
1071 timeout_ms: Option<u32>,
1072 ) -> Result<BiStream, JsValue> {
1073 let (_connection_id, remote_node_id, send, recv) = self
1074 .inner
1075 .open_peer_bi_transport_only(&id, timeout_ms.map(|value| value as u64))
1076 .await
1077 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1078 Ok(BiStream::from_parts(send, recv, remote_node_id))
1079 }
1080
1081 pub async fn open_peer_native_bi(
1091 &self,
1092 id: String,
1093 label: String,
1094 timeout_ms: Option<u32>,
1095 ) -> Result<BiStream, JsValue> {
1096 if label != "drive-view" {
1097 return Err(JsValue::from_str(
1098 "unsupported native peer stream label; only drive-view is allowed",
1099 ));
1100 }
1101
1102 let (_connection_id, remote_node_id, mut send, recv) = self
1103 .inner
1104 .open_peer_bi(&id, timeout_ms.map(|value| value as u64))
1105 .await
1106 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1107 let envelope = crate::stream_metadata::encode_channel_envelope(&label, None)
1108 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1109 send.write_all(&envelope)
1110 .await
1111 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1112
1113 Ok(BiStream::from_peer_parts(send, recv, remote_node_id))
1114 }
1115
1116 pub async fn open_uni(&self, endpoint_id: String) -> Result<PeerUniStream, JsValue> {
1117 let endpoint_id: iroh::EndpointId = endpoint_id
1118 .parse()
1119 .map_err(|e| JsValue::from_str(&format!("{}", e)))?;
1120 self.inner
1121 .assert_raw_peer_stream_allowed(&endpoint_id)
1122 .await
1123 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1124 self.inner
1127 .ensure_connection_manager_record_before_peer_stream(&endpoint_id)
1128 .await
1129 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1130 let node_guard = self.inner.iroh_node.read().await;
1131 if let Some(node) = node_guard.as_ref() {
1132 let send = node
1133 .open_uni(endpoint_id.clone())
1134 .await
1135 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1136 Ok(peer_uni_stream_from_send(
1137 crate::application_crypto_streams::PeerSendStream::plain(send),
1138 ))
1139 } else {
1140 Err(JsValue::from_str("Iroh node not initialized"))
1141 }
1142 }
1143
1144 pub async fn open_peer_uni(
1145 &self,
1146 id: String,
1147 timeout_ms: Option<u32>,
1148 ) -> Result<PeerUniStream, JsValue> {
1149 let (_connection_id, _remote_node_id, send) = self
1150 .inner
1151 .open_peer_uni(&id, timeout_ms.map(|value| value as u64))
1152 .await
1153 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1154 Ok(peer_uni_stream_from_send(send))
1155 }
1156
1157 pub async fn send_peer(&self, id: String, data: Vec<u8>) -> Result<(), JsValue> {
1167 self.inner
1168 .send_peer(&id, &data)
1169 .await
1170 .map_err(|e| JsValue::from_str(&e.to_string()))
1171 }
1172
1173 pub async fn iroh_path_kind(&self, peer_id: String) -> String {
1177 match self.inner.iroh_path_kind(&peer_id).await {
1178 crate::client::IrohPathKind::DirectQuic => "direct-quic".to_string(),
1179 crate::client::IrohPathKind::DirectLan => "direct-lan".to_string(),
1180 crate::client::IrohPathKind::Relay => "relay".to_string(),
1181 crate::client::IrohPathKind::Ble => "ble".to_string(),
1182 crate::client::IrohPathKind::Unknown => "unknown".to_string(),
1183 }
1184 }
1185
1186 pub async fn iroh_transport_rtt_ms(&self, peer_id: String) -> Option<u32> {
1189 self.inner
1190 .iroh_transport_rtt_ms(&peer_id)
1191 .await
1192 .map(|value| value.min(u32::MAX as u64) as u32)
1193 }
1194
1195 pub async fn incoming_streams(&self) -> Result<JsReadableStream, JsValue> {
1196 let (node, stream) = {
1197 let node_guard = self.inner.iroh_node.read().await;
1198 if let Some(node) = node_guard.as_ref() {
1199 (node.clone(), node.incoming_streams_stream())
1200 } else {
1201 return Err(JsValue::from_str("Iroh node not initialized"));
1202 }
1203 };
1204
1205 use futures::StreamExt;
1206 let mapped_stream = stream.filter_map(move |incoming| {
1207 let node = node.clone();
1208 async move {
1209 node.incoming_stream_is_current(&incoming)
1210 .await
1211 .then(|| crate::wasm_node::BiStream::incoming_to_js_value(incoming))
1212 }
1213 });
1214
1215 Ok(wasm_streams::ReadableStream::from_stream(mapped_stream).into_raw())
1216 }
1217
1218 pub async fn update_presence(
1219 &self,
1220 user_id: String,
1221 device_name: String,
1222 ticket: String,
1223 metadata: Option<String>,
1224 ttl_ms: Option<u64>,
1225 ) -> Result<(), JsValue> {
1226 self.inner
1227 .update_presence_with_ttl(
1228 &user_id,
1229 &device_name,
1230 &ticket,
1231 ttl_ms.unwrap_or(300_000),
1232 metadata.as_deref(),
1233 )
1234 .await
1235 .map_err(|e| JsValue::from_str(&e.to_string()))
1236 }
1237
1238 pub async fn send_message(
1239 &self,
1240 target_id: String,
1241 payload: String,
1242 state: Option<String>,
1243 reply_payload: Option<String>,
1244 ) -> Result<String, JsValue> {
1245 self.inner
1246 .send_message(
1247 &target_id,
1248 &payload,
1249 state.as_deref(),
1250 reply_payload.as_deref(),
1251 )
1252 .await
1253 .map_err(|e| JsValue::from_str(&e.to_string()))
1254 }
1255
1256 pub async fn set_offline(&self, user_id: String) -> Result<(), JsValue> {
1257 self.inner
1258 .set_offline(&user_id)
1259 .await
1260 .map_err(|e| JsValue::from_str(&e.to_string()))
1261 }
1262
1263 pub async fn update_device(
1264 &self,
1265 user_id: String,
1266 device_id: String,
1267 device_name: Option<String>,
1268 capabilities: Option<JsValue>,
1269 metadata: Option<String>,
1270 ) -> Result<(), JsValue> {
1271 let parsed_capabilities = match capabilities {
1272 Some(value) if !value.is_null() && !value.is_undefined() => Some(
1273 serde_wasm_bindgen::from_value::<crate::signaling::DeviceCapabilities>(value)
1274 .map_err(|e| JsValue::from_str(&e.to_string()))?,
1275 ),
1276 _ => None,
1277 };
1278
1279 self.inner
1280 .update_device(
1281 &user_id,
1282 &device_id,
1283 device_name.as_deref(),
1284 parsed_capabilities,
1285 metadata.as_deref(),
1286 )
1287 .await
1288 .map_err(|e| JsValue::from_str(&e.to_string()))
1289 }
1290
1291 pub async fn delete_device(
1292 &self,
1293 user_id: String,
1294 device_id: String,
1295 ) -> Result<(), JsValue> {
1296 self.inner
1297 .delete_device(&user_id, &device_id)
1298 .await
1299 .map_err(|e| JsValue::from_str(&e.to_string()))
1300 }
1301
1302 pub fn force_reconnect_snapshot(&self) {
1303 self.inner.clone().force_reconnect_snapshot();
1304 }
1305
1306 pub fn stop_presence_loop(&self) {
1307 self.inner.stop_presence_loop();
1308 }
1309
1310 pub fn stop_auto_connect(&self) {
1311 self.inner.stop_auto_connect();
1312 self.inner.stop_browser_auto_connect();
1313 }
1314
1315 pub fn start_auto_connect(
1316 &self,
1317 user_id: String,
1318 local_device_id: String,
1319 ) -> Result<(), JsValue> {
1320 self.inner
1321 .start_browser_auto_connect(user_id, local_device_id)
1322 .map_err(|error| JsValue::from_str(&error.to_string()))
1323 }
1324
1325 pub fn submit_browser_desired_peers(
1326 &self,
1327 revision: u32,
1328 peers_json: String,
1329 ) -> Result<bool, JsValue> {
1330 self.inner
1331 .submit_browser_desired_peers(u64::from(revision), &peers_json)
1332 .map_err(|error| JsValue::from_str(&error.to_string()))
1333 }
1334
1335 pub fn wake_browser_auto_connect(&self) -> bool {
1336 self.inner.wake_browser_auto_connect()
1337 }
1338
1339 pub async fn set_auto_connect_excluded(&self, device_id: String, excluded: bool) {
1340 if excluded {
1341 self.inner.exclude_peer_and_publish(&device_id).await;
1342 } else {
1343 self.inner.unexclude_peer_and_publish(&device_id).await;
1344 }
1345 self.inner.wake_browser_auto_connect();
1346 }
1347
1348 pub fn is_auto_connect_excluded(&self, device_id: String) -> bool {
1349 self.inner.is_auto_connect_excluded(&device_id)
1350 }
1351
1352 pub async fn disconnect_device(
1353 &self,
1354 device_id: String,
1355 node_id_hint: Option<String>,
1356 ) -> Result<JsValue, JsValue> {
1357 let retired = self
1358 .inner
1359 .disconnect_device(&device_id, node_id_hint.as_deref())
1360 .await;
1361 serde_wasm_bindgen::to_value(&retired).map_err(|e| JsValue::from_str(&e.to_string()))
1362 }
1363
1364 pub fn stop_auth_scoped_activity(&self) {
1365 self.inner.stop_auth_scoped_activity();
1366 self.inner.stop_browser_auto_connect();
1367 }
1368
1369 pub fn start_presence_loop(
1370 &self,
1371 user_id: String,
1372 device_name: String,
1373 ticket: String,
1374 metadata: Option<String>,
1375 ) {
1376 self.inner
1377 .clone()
1378 .start_signaling_loop(user_id, device_name, ticket, metadata);
1379 }
1380
1381 pub async fn search_devices(&self, user_id: String) -> Result<JsValue, JsValue> {
1382 let devices = self
1383 .inner
1384 .search_devices(&user_id)
1385 .await
1386 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1387 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
1388 }
1389
1390 pub async fn devices_with_status(&self, user_id: String) -> Result<JsValue, JsValue> {
1391 let devices = self
1392 .inner
1393 .devices_with_status(&user_id)
1394 .await
1395 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1396 serde_wasm_bindgen::to_value(&devices).map_err(|e| JsValue::from_str(&e.to_string()))
1397 }
1398
1399 pub async fn connect_device(
1400 &self,
1401 device_id: Option<String>,
1402 endpoint_ticket: String,
1403 ) -> Result<JsValue, JsValue> {
1404 let result = self
1405 .inner
1406 .connect_device(device_id.as_deref(), &endpoint_ticket)
1407 .await
1408 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1409 serde_wasm_bindgen::to_value(&result).map_err(|e| JsValue::from_str(&e.to_string()))
1410 }
1411
1412 pub async fn create_session(&self, session_json: String) -> Result<(), JsValue> {
1413 let session: crate::signaling::SignalingSession =
1414 serde_json::from_str(&session_json)
1415 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1416 self.inner
1417 .create_session(session)
1418 .await
1419 .map_err(|e| JsValue::from_str(&e.to_string()))
1420 }
1421
1422 pub async fn update_session(
1423 &self,
1424 session_id: String,
1425 update_json: String,
1426 ) -> Result<(), JsValue> {
1427 let update_data: serde_json::Value = serde_json::from_str(&update_json)
1428 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1429 self.inner
1430 .update_session(&session_id, update_data)
1431 .await
1432 .map_err(|e| JsValue::from_str(&e.to_string()))
1433 }
1434
1435 pub async fn create_room(
1438 &self,
1439 room_id: String,
1440 user_id: String,
1441 ticket_str: String,
1442 my_node_id: String,
1443 tag: String,
1444 max_members: Option<u32>,
1445 ) -> Result<bool, JsValue> {
1446 self.inner
1447 .room
1448 .create_room(
1449 &room_id,
1450 &user_id,
1451 &ticket_str,
1452 &my_node_id,
1453 &tag,
1454 max_members,
1455 )
1456 .await
1457 .map_err(|e| JsValue::from_str(&e.to_string()))
1458 }
1459
1460 pub async fn join_room(
1461 &self,
1462 room_id: String,
1463 user_id: String,
1464 ticket_str: String,
1465 my_node_id: String,
1466 tag: String,
1467 ) -> Result<(), JsValue> {
1468 self.inner
1469 .room
1470 .join_room(&room_id, &user_id, &ticket_str, &my_node_id, &tag)
1471 .await
1472 .map_err(|e| JsValue::from_str(&e.to_string()))
1473 }
1474
1475 pub async fn get_members(
1476 &self,
1477 room_id: String,
1478 my_node_id: String,
1479 tag: String,
1480 ) -> Result<String, JsValue> {
1481 let members = self
1482 .inner
1483 .room
1484 .get_members(&room_id, &my_node_id, &tag)
1485 .await
1486 .map_err(|e| JsValue::from_str(&e.to_string()))?;
1487 serde_json::to_string(&members).map_err(|e| JsValue::from_str(&e.to_string()))
1488 }
1489
1490 pub async fn leave_room(
1491 &self,
1492 room_id: String,
1493 my_node_id: String,
1494 tag: String,
1495 ) -> Result<(), JsValue> {
1496 self.inner
1497 .room
1498 .leave_room(&room_id, &my_node_id, &tag)
1499 .await
1500 .map_err(|e| JsValue::from_str(&e.to_string()))
1501 }
1502
1503 pub async fn heartbeat_tick(
1504 &self,
1505 room_id: String,
1506 member_id: String,
1507 tag: String,
1508 ) -> Result<(), JsValue> {
1509 self.inner
1510 .room
1511 .heartbeat_tick(&room_id, &member_id, &tag)
1512 .await
1513 .map_err(|e| JsValue::from_str(&e.to_string()))
1514 }
1515 }
1516
1517 #[wasm_bindgen(start)]
1518 pub fn start() {
1519 console_error_panic_hook::set_once();
1520 }
1521}