diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 034be6457..162b46c1f 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -2001,6 +2001,11 @@ impl CommandApi { Ok(()) } + /// Leaves the gossip of the webxdc with the given message id. + /// + /// NB: When this is called before closing a webxdc app in UIs, it must be guaranteed that + /// `send_webxdc_realtime_*()` functions aren't called for the given `instance_message_id` + /// anymore until the app is open again. async fn leave_webxdc_realtime(&self, account_id: u32, instance_message_id: u32) -> Result<()> { let ctx = self.get_context(account_id).await?; leave_webxdc_realtime(&ctx, MsgId::new(instance_message_id)).await diff --git a/src/peer_channels.rs b/src/peer_channels.rs index 53d37927d..0299affad 100644 --- a/src/peer_channels.rs +++ b/src/peer_channels.rs @@ -278,18 +278,24 @@ impl Context { }) } + /// Returns [`None`] if the peer channels has not been initialized. + pub async fn get_peer_channels(&self) -> Option> { + tokio::sync::RwLockReadGuard::<'_, std::option::Option>::try_map( + self.iroh.read().await, + |opt_iroh| opt_iroh.as_ref(), + ) + .ok() + } + /// Get or initialize the iroh peer channel. pub async fn get_or_try_init_peer_channel( &self, ) -> Result> { if !self.get_config_bool(Config::WebxdcRealtimeEnabled).await? { - bail!("Attempt to get Iroh when realtime is disabled"); + bail!("Attempt to initialize Iroh when realtime is disabled"); } - if let Ok(lock) = tokio::sync::RwLockReadGuard::<'_, std::option::Option>::try_map( - self.iroh.read().await, - |opt_iroh| opt_iroh.as_ref(), - ) { + if let Some(lock) = self.get_peer_channels().await { return Ok(lock); } @@ -479,14 +485,17 @@ pub async fn send_webxdc_realtime_data(ctx: &Context, msg_id: MsgId, data: Vec Result<()> { - if !ctx.get_config_bool(Config::WebxdcRealtimeEnabled).await? { + let Some(iroh) = ctx.get_peer_channels().await else { return Ok(()); - } - let topic = get_iroh_topic_for_msg(ctx, msg_id) - .await? - .with_context(|| format!("Message {msg_id} has no gossip topic"))?; - let iroh = ctx.get_or_try_init_peer_channel().await?; + }; + let Some(topic) = get_iroh_topic_for_msg(ctx, msg_id).await? else { + return Ok(()); + }; iroh.leave_realtime(topic).await?; info!(ctx, "IROH_REALTIME: Left gossip for message {msg_id}"); @@ -1110,7 +1119,6 @@ mod tests { assert!(alice.ctx.iroh.read().await.is_none()); - // creates iroh endpoint as side effect leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap(); assert!(alice.ctx.iroh.read().await.is_none()); @@ -1119,4 +1127,19 @@ mod tests { // if accidentally called with the setting disabled. assert!(alice.ctx.get_or_try_init_peer_channel().await.is_err()); } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_leave_webxdc_realtime_uninitialized() { + let mut tcm = TestContextManager::new(); + let alice = &mut tcm.alice().await; + + alice + .set_config_bool(Config::WebxdcRealtimeEnabled, true) + .await + .unwrap(); + + assert!(alice.ctx.iroh.read().await.is_none()); + leave_webxdc_realtime(alice, MsgId::new(1)).await.unwrap(); + assert!(alice.ctx.iroh.read().await.is_none()); + } }