@@ -254,6 +254,7 @@ impl IrohState {
254254 event_target : Option < EventTarget > ,
255255 app_handle : Option < AppHandle > ,
256256 label : String ,
257+ ws_event_targets : Option < Arc < std:: sync:: RwLock < HashMap < String , tokio:: sync:: mpsc:: Sender < Vec < u8 > > > > > > ,
257258 ) -> Result < ( bool , Option < oneshot:: Receiver < ( ) > > ) > {
258259 let mut channels = self . channels . write ( ) . await ;
259260
@@ -265,6 +266,15 @@ impl IrohState {
265266 let mut state = channel_state. event_target . write ( ) . unwrap_or_else ( |e| { log_error ! ( "[WEBXDC] RwLock poisoned — recovering" ) ; e. into_inner ( ) } ) ;
266267 state. set_target ( target) ; // Flushes buffered events from preconnect phase
267268 }
269+ // Wire up WS sender for bi-directional receive (if WS is connected)
270+ if let Some ( ref senders) = ws_event_targets {
271+ let map = senders. read ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
272+ if let Some ( ws_tx) = map. get ( & label) {
273+ let mut state = channel_state. event_target . write ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
274+ state. set_ws_sender ( ws_tx. clone ( ) ) ;
275+ log_info ! ( "[WEBXDC] RT WS bi-directional enabled for: {label}" ) ;
276+ }
277+ }
268278 return Ok ( ( true , None ) ) ;
269279 }
270280
@@ -276,7 +286,20 @@ impl IrohState {
276286 peer_ids. len( )
277287 ) ;
278288
279- // Connect to peers so gossip can discover them
289+ // DON'T manually connect + handle_connection here — that creates
290+ // connections BEFORE the topic subscription exists, causing a race
291+ // where messages arrive for an unregistered topic and get lost.
292+ // Instead, connect AFTER subscribing, so the gossip actor has the
293+ // topic registered when the connection delivers messages.
294+
295+ let ( join_tx, join_rx) = oneshot:: channel ( ) ;
296+
297+ let gossip_topic = self
298+ . gossip
299+ . subscribe_with_opts ( topic, JoinOptions :: with_bootstrap ( peer_ids) )
300+ . await ?;
301+
302+ // NOW connect — topic subscription is registered, safe to receive
280303 for peer_addr in & peers {
281304 if !peer_addr. addrs . is_empty ( ) {
282305 let addr = peer_addr. clone ( ) ;
@@ -294,19 +317,22 @@ impl IrohState {
294317 } ) ;
295318 }
296319 }
297-
298- let ( join_tx, join_rx) = oneshot:: channel ( ) ;
299-
300- let gossip_topic = self
301- . gossip
302- . subscribe_with_opts ( topic, JoinOptions :: with_bootstrap ( peer_ids) )
303- . await ?;
304320 let ( gossip_sender, gossip_receiver) = gossip_topic. split ( ) ;
305321
306322 // Create shared event target for the subscribe loop (buffers events if target is None)
307323 let shared_event_target: SharedEventTarget = Arc :: new ( std:: sync:: RwLock :: new ( EventTargetState :: new ( event_target) ) ) ;
308324 let shared_target_clone = shared_event_target. clone ( ) ;
309325
326+ // Wire up WS sender for bi-directional receive (if WS is connected)
327+ if let Some ( ref senders) = ws_event_targets {
328+ let map = senders. read ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
329+ if let Some ( ws_tx) = map. get ( & label) {
330+ let mut state = shared_event_target. write ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
331+ state. set_ws_sender ( ws_tx. clone ( ) ) ;
332+ log_info ! ( "[WEBXDC] RT WS bi-directional enabled for: {label}" ) ;
333+ }
334+ }
335+
310336 // Create shared peer count
311337 let shared_peer_count: SharedPeerCount = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
312338 let peer_count_clone = shared_peer_count. clone ( ) ;
@@ -391,11 +417,12 @@ impl IrohState {
391417
392418 log_trace ! ( "[WEBXDC] add_peer: Connecting to peer {}" , peer_addr. id) ;
393419
394- // Connect to the peer and hand the connection to gossip
420+ // Connect and hand to gossip, then join_peers.
421+ // Topic subscription already exists (channel is in the map),
422+ // so the connection won't race with topic registration.
395423 let conn = self . endpoint . connect ( peer_addr, GOSSIP_ALPN ) . await ?;
396424 self . gossip . handle_connection ( conn) . await ?;
397425
398- // Join the peer to the existing gossip topic
399426 let channels = self . channels . read ( ) . await ;
400427 if let Some ( channel_state) = channels. get ( topic) {
401428 channel_state. sender . join_peers ( vec ! [ peer. id] ) . await ?;
@@ -459,6 +486,9 @@ impl IrohState {
459486 // 1. Remove fast-path SendHandle (drops its GossipSender clone)
460487 self . send_handles . write ( ) . unwrap_or_else ( |e| { log_error ! ( "[WEBXDC] RwLock poisoned — recovering" ) ; e. into_inner ( ) } ) . remove ( label) ;
461488
489+ // Remove WS sender for this label (ws_senders is on RealtimeManager,
490+ // but we're on IrohState — caller handles this separately)
491+
462492 if let Some ( channel) = self . channels . write ( ) . await . remove ( & topic) {
463493 // 2. Drop the ChannelState's sender explicitly (don't wait for implicit drop)
464494 drop ( channel. sender ) ;
@@ -467,7 +497,7 @@ impl IrohState {
467497 channel. subscribe_loop . abort ( ) ;
468498 let _ = channel. subscribe_loop . await ;
469499
470- // 4. Small yield to let the runtime fully clean up dropped tasks
500+ // 4. Yield to let the gossip actor process the quit
471501 tokio:: task:: yield_now ( ) . await ;
472502
473503 log_info ! ( "Left realtime channel {:?}" , topic) ;
@@ -515,19 +545,42 @@ pub enum EventTarget {
515545pub ( crate ) struct EventTargetState {
516546 target : Option < EventTarget > ,
517547 buffer : Vec < RealtimeEvent > ,
548+ /// Optional WebSocket sender for bi-directional WS (bypasses JNI on Android).
549+ /// When set, Data events are sent directly through WS instead of the normal target.
550+ ws_sender : Option < tokio:: sync:: mpsc:: Sender < Vec < u8 > > > ,
518551}
519552
520553impl EventTargetState {
521554 fn new ( target : Option < EventTarget > ) -> Self {
522- Self { target, buffer : Vec :: new ( ) }
555+ Self { target, buffer : Vec :: new ( ) , ws_sender : None }
523556 }
524557
525- /// Send an event, buffering if no target is set yet
558+ /// Register a WS sender for bi-directional receive. Data events bypass
559+ /// the normal target (JNI on Android) and go straight through WebSocket.
560+ pub fn set_ws_sender ( & mut self , sender : tokio:: sync:: mpsc:: Sender < Vec < u8 > > ) {
561+ self . ws_sender = Some ( sender) ;
562+ }
563+
564+ pub fn clear_ws_sender ( & mut self ) {
565+ self . ws_sender = None ;
566+ }
567+
568+ /// Send an event, buffering if no target is set yet.
569+ /// Data events are routed through WebSocket when available (bypasses JNI on Android).
526570 fn send ( & mut self , event : RealtimeEvent ) -> bool {
571+ // If WS sender is available and this is a Data event, send via WS directly.
572+ // This bypasses the JNI/evaluateJavascript path that gets starved by WASM.
573+ if let Some ( ref ws_tx) = self . ws_sender {
574+ if let RealtimeEvent :: Data ( ref b91_data) = event {
575+ // Send raw base91 string as binary WS frame
576+ let _ = ws_tx. try_send ( b91_data. as_bytes ( ) . to_vec ( ) ) ;
577+ return true ;
578+ }
579+ }
580+
527581 if let Some ( ref target) = self . target {
528582 Self :: deliver ( target, event)
529583 } else {
530- // Buffer up to 256 events (prevents unbounded memory if target is never set)
531584 if self . buffer . len ( ) < 256 {
532585 self . buffer . push ( event) ;
533586 }
@@ -764,6 +817,9 @@ pub struct RealtimeManager {
764817 /// Fast-path send handles — owned here so the WS server can start
765818 /// before IrohState exists (critical for Android JNI timing).
766819 send_handles : Arc < std:: sync:: RwLock < HashMap < String , SendHandle > > > ,
820+ /// Map of window_label → WS sender for bi-directional receive.
821+ /// WS handler registers sender on connect, join_channel wires it into the event target.
822+ pub ( crate ) ws_senders : Arc < std:: sync:: RwLock < HashMap < String , tokio:: sync:: mpsc:: Sender < Vec < u8 > > > > > ,
767823}
768824
769825impl RealtimeManager {
@@ -773,6 +829,7 @@ impl RealtimeManager {
773829 relay_url,
774830 ws_info : std:: sync:: OnceLock :: new ( ) ,
775831 send_handles : Arc :: new ( std:: sync:: RwLock :: new ( HashMap :: new ( ) ) ) ,
832+ ws_senders : Arc :: new ( std:: sync:: RwLock :: new ( HashMap :: new ( ) ) ) ,
776833 }
777834 }
778835
@@ -831,6 +888,7 @@ impl RealtimeManager {
831888
832889 // Spawn accept loop on the MAIN Tauri runtime (survives JNI temp runtime)
833890 let send_handles = self . send_handles . clone ( ) ;
891+ let ws_senders = self . ws_senders . clone ( ) ;
834892 tauri:: async_runtime:: spawn ( async move {
835893 // Convert std listener to tokio listener on the main runtime
836894 let listener = match tokio:: net:: TcpListener :: from_std ( std_listener) {
@@ -840,7 +898,7 @@ impl RealtimeManager {
840898 return ;
841899 }
842900 } ;
843- super :: rt_ws:: run_accept_loop ( listener, token, send_handles) . await ;
901+ super :: rt_ws:: run_accept_loop ( listener, token, send_handles, ws_senders ) . await ;
844902 } ) ;
845903 }
846904
0 commit comments