From 3fd4556638e1e4f3c9f569b35fddb55e4f2189fc Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Aug 2025 14:12:38 +0200 Subject: [PATCH 01/17] chore(sdk): Simplify code in `listen_to_event_cache_and_send_queue_updates_task`. This patch removes the intermediate `rooms` variable in a new block. The read-lock can be used immediately. --- crates/matrix-sdk/src/latest_events/mod.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 92b17a6c7c0..2418f6d879a 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -537,14 +537,11 @@ async fn listen_to_event_cache_and_send_queue_updates_task( // Initialise the list of rooms that are listened. // - // Technically, we can use `rooms` to get this information, but it would involve - // a read-lock. In order to reduce the pressure on this lock, we use - // this intermediate structure. - let mut listened_rooms = { - let rooms = registered_rooms.rooms.read().await; - - HashSet::from_iter(rooms.keys().cloned()) - }; + // Technically, we can use `registered_rooms.rooms` every time to get this + // information, but it would involve a read-lock. In order to reduce the + // pressure on this lock, we use this intermediate structure. + let mut listened_rooms = + HashSet::from_iter(registered_rooms.rooms.read().await.keys().cloned()); loop { if listen_to_event_cache_and_send_queue_updates( From 95b56a9c7e3d33a2d5cca8ae8ec49791102443c8 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Aug 2025 15:50:25 +0200 Subject: [PATCH 02/17] feat(sdk): `LatestEvents` listens to the `SendQueue`. This patch updates `LatestEvents` to listen to the updates from the `SendQueue`. The `listen_to_event_cache_and_send_queue_updates` function contains the important change. A new `LatestEventQueueUpdate` enum is added to represent either an update from the event cache, or from the send queue. So far, `compute_latest_events` does nothing in particular, apart from panicking with a `todo!()` when a send queue update is met. --- crates/matrix-sdk/src/latest_events/mod.rs | 214 +++++++++++++++++++-- 1 file changed, 196 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 2418f6d879a..d43ac9d51bb 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -57,19 +57,22 @@ use std::{ pub use error::LatestEventsError; use eyeball::{AsyncLock, Subscriber}; -use futures_util::{select, FutureExt}; +use futures_util::FutureExt; use latest_event::LatestEvent; pub use latest_event::LatestEventValue; use matrix_sdk_common::executor::{spawn, AbortOnDrop, JoinHandleExt as _}; use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId}; -use tokio::sync::{broadcast, mpsc, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::{ + select, + sync::{broadcast, mpsc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; use tracing::error; use crate::{ client::WeakClient, event_cache::{EventCache, EventCacheError, RoomEventCache, RoomEventCacheGenericUpdate}, room::WeakRoom, - send_queue::SendQueue, + send_queue::{RoomSendQueueUpdate, SendQueue, SendQueueUpdate}, }; /// The entry point to fetch the [`LatestEventValue`] for rooms or threads. @@ -408,6 +411,25 @@ enum RoomRegistration { Remove(OwnedRoomId), } +/// Represents the kind of updates the [`compute_latest_events_task`] will have +/// to deal with. +enum LatestEventQueueUpdate { + /// An update from the [`EventCache`] happened. + EventCache { + /// The ID of the room that has triggered the update. + room_id: OwnedRoomId, + }, + + /// An update from the [`SendQueue`] happened. + SendQueue { + /// The ID of the room that has triggered the update. + room_id: OwnedRoomId, + + /// The update itself. + update: RoomSendQueueUpdate, + }, +} + /// Type holding the [`LatestEvent`] for a room and for all its threads. #[derive(Debug)] struct RoomLatestEvents { @@ -529,11 +551,12 @@ async fn listen_to_event_cache_and_send_queue_updates_task( registered_rooms: Arc, mut room_registration_receiver: mpsc::Receiver, event_cache: EventCache, - _send_queue: SendQueue, - latest_event_queue_sender: mpsc::UnboundedSender, + send_queue: SendQueue, + latest_event_queue_sender: mpsc::UnboundedSender, ) { let mut event_cache_generic_updates_subscriber = event_cache.subscribe_to_room_generic_updates(); + let mut send_queue_generic_updates_subscriber = send_queue.subscribe(); // Initialise the list of rooms that are listened. // @@ -547,6 +570,7 @@ async fn listen_to_event_cache_and_send_queue_updates_task( if listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut event_cache_generic_updates_subscriber, + &mut send_queue_generic_updates_subscriber, &mut listened_rooms, &latest_event_queue_sender, ) @@ -567,10 +591,15 @@ async fn listen_to_event_cache_and_send_queue_updates_task( async fn listen_to_event_cache_and_send_queue_updates( room_registration_receiver: &mut mpsc::Receiver, event_cache_generic_updates_subscriber: &mut broadcast::Receiver, + send_queue_generic_updates_subscriber: &mut broadcast::Receiver, listened_rooms: &mut HashSet, - latest_event_queue_sender: &mpsc::UnboundedSender, + latest_event_queue_sender: &mpsc::UnboundedSender, ) -> ControlFlow<()> { + // We need a biased select here: `room_registration_receiver` must have the + // priority over other futures. select! { + biased; + update = room_registration_receiver.recv().fuse() => { match update { Some(RoomRegistration::Add(room_id)) => { @@ -592,7 +621,9 @@ async fn listen_to_event_cache_and_send_queue_updates( let room_id = room_event_cache_generic_update.room_id; if listened_rooms.contains(&room_id) { - let _ = latest_event_queue_sender.send(room_id); + let _ = latest_event_queue_sender.send(LatestEventQueueUpdate::EventCache { + room_id + }); } } else { error!("`event_cache_generic_updates` channel has been closed"); @@ -600,6 +631,21 @@ async fn listen_to_event_cache_and_send_queue_updates( return ControlFlow::Break(()); } } + + send_queue_generic_update = send_queue_generic_updates_subscriber.recv().fuse() => { + if let Ok(SendQueueUpdate { room_id, update }) = send_queue_generic_update { + if listened_rooms.contains(&room_id) { + let _ = latest_event_queue_sender.send(LatestEventQueueUpdate::SendQueue { + room_id, + update + }); + } + } else { + error!("`send_queue_generic_updates` channel has been closed"); + + return ControlFlow::Break(()); + } + } } ControlFlow::Continue(()) @@ -612,7 +658,7 @@ async fn listen_to_event_cache_and_send_queue_updates( /// [`listen_to_event_cache_and_send_queue_updates_task`]. async fn compute_latest_events_task( registered_rooms: Arc, - mut latest_event_queue_receiver: mpsc::UnboundedReceiver, + mut latest_event_queue_receiver: mpsc::UnboundedReceiver, ) { const BUFFER_SIZE: usize = 16; @@ -626,16 +672,29 @@ async fn compute_latest_events_task( error!("`compute_latest_events_task` has stopped"); } -async fn compute_latest_events(registered_rooms: &RegisteredRooms, for_rooms: &[OwnedRoomId]) { - for room_id in for_rooms { - let mut rooms = registered_rooms.rooms.write().await; +async fn compute_latest_events( + registered_rooms: &RegisteredRooms, + latest_event_queue_updates: &[LatestEventQueueUpdate], +) { + for latest_event_queue_update in latest_event_queue_updates { + match latest_event_queue_update { + LatestEventQueueUpdate::EventCache { room_id } => { + let mut rooms = registered_rooms.rooms.write().await; - if let Some(room_latest_events) = rooms.get_mut(room_id) { - room_latest_events.update().await; - } else { - error!(?room_id, "Failed to find the room"); + if let Some(room_latest_events) = rooms.get_mut(room_id) { + room_latest_events.update().await; + } else { + error!(?room_id, "Failed to find the room"); - continue; + continue; + } + } + + LatestEventQueueUpdate::SendQueue { room_id, update } => { + // let mut rooms = registered_rooms.rooms.write().await; + + todo!() + } } } } @@ -650,12 +709,12 @@ mod tests { RoomState, }; use matrix_sdk_test::{async_test, event_factory::EventFactory, JoinedRoomBuilder}; - use ruma::{event_id, owned_room_id, room_id, user_id}; + use ruma::{event_id, owned_room_id, room_id, user_id, OwnedTransactionId}; use stream_assert::assert_pending; use super::{ broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventValue, - RoomEventCacheGenericUpdate, RoomRegistration, + RoomEventCacheGenericUpdate, RoomRegistration, RoomSendQueueUpdate, SendQueueUpdate, }; use crate::test_utils::mocks::MatrixMockServer; @@ -819,6 +878,8 @@ mod tests { let (room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); let (_room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = broadcast::channel(1); + let (_send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); let mut listened_rooms = HashSet::new(); let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); @@ -831,6 +892,7 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -850,6 +912,7 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -873,6 +936,7 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -892,6 +956,8 @@ mod tests { let (_room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); let (_room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = broadcast::channel(1); + let (_send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); let mut listened_rooms = HashSet::new(); let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); @@ -902,6 +968,7 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -920,6 +987,8 @@ mod tests { let (room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); let (room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = broadcast::channel(1); + let (_send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); let mut listened_rooms = HashSet::new(); let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); @@ -933,6 +1002,7 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -958,6 +1028,82 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, + &mut listened_rooms, + &latest_event_queue_sender, + ) + .await + .is_continue()); + } + + assert_eq!(listened_rooms.len(), 1); + assert!(listened_rooms.contains(&room_id)); + + // A latest event computation has been triggered! + assert!(latest_event_queue_receiver.is_empty().not()); + } + } + + #[async_test] + async fn test_inputs_task_can_listen_to_send_queue() { + let room_id = owned_room_id!("!r0"); + + let (room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); + let (_room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = + broadcast::channel(1); + let (send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); + let mut listened_rooms = HashSet::new(); + let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); + + // New send queue update, but the `LatestEvents` isn't listening to it. + { + send_queue_generic_update_sender + .send(SendQueueUpdate { + room_id: room_id.clone(), + update: RoomSendQueueUpdate::SentEvent { + transaction_id: OwnedTransactionId::from("txnid0"), + event_id: event_id!("$ev0").to_owned(), + }, + }) + .unwrap(); + + // Run the task. + assert!(listen_to_event_cache_and_send_queue_updates( + &mut room_registration_receiver, + &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, + &mut listened_rooms, + &latest_event_queue_sender, + ) + .await + .is_continue()); + + assert!(listened_rooms.is_empty()); + + // No latest event computation has been triggered. + assert!(latest_event_queue_receiver.is_empty()); + } + + // New send queue update, but this time, the `LatestEvents` is listening to it. + { + room_registration_sender.send(RoomRegistration::Add(room_id.clone())).await.unwrap(); + send_queue_generic_update_sender + .send(SendQueueUpdate { + room_id: room_id.clone(), + update: RoomSendQueueUpdate::SentEvent { + transaction_id: OwnedTransactionId::from("txnid1"), + event_id: event_id!("$ev1").to_owned(), + }, + }) + .unwrap(); + + // Run the task to handle the `RoomRegistration` and the `SendQueueUpdate`. + for _ in 0..2 { + assert!(listen_to_event_cache_and_send_queue_updates( + &mut room_registration_receiver, + &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) @@ -978,6 +1124,8 @@ mod tests { let (_room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); let (room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = broadcast::channel(1); + let (_send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); let mut listened_rooms = HashSet::new(); let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); @@ -988,6 +1136,36 @@ mod tests { assert!(listen_to_event_cache_and_send_queue_updates( &mut room_registration_receiver, &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, + &mut listened_rooms, + &latest_event_queue_sender, + ) + .await + // It breaks! + .is_break()); + + assert_eq!(listened_rooms.len(), 0); + assert!(latest_event_queue_receiver.is_empty()); + } + + #[async_test] + async fn test_inputs_task_stops_when_send_queue_channel_is_closed() { + let (_room_registration_sender, mut room_registration_receiver) = mpsc::channel(1); + let (_room_event_cache_generic_update_sender, mut room_event_cache_generic_update_receiver) = + broadcast::channel(1); + let (send_queue_generic_update_sender, mut send_queue_generic_update_receiver) = + broadcast::channel(1); + let mut listened_rooms = HashSet::new(); + let (latest_event_queue_sender, latest_event_queue_receiver) = mpsc::unbounded_channel(); + + // Drop the sender to close the channel. + drop(send_queue_generic_update_sender); + + // Run the task. + assert!(listen_to_event_cache_and_send_queue_updates( + &mut room_registration_receiver, + &mut room_event_cache_generic_update_receiver, + &mut send_queue_generic_update_receiver, &mut listened_rooms, &latest_event_queue_sender, ) From 4d6d44894e67ffd0fe803d5a76d31dee215bcd24 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Aug 2025 16:26:54 +0200 Subject: [PATCH 03/17] chore(sdk): Rename `update` to `update_with_event_cache` in `latest_events`. This patch renames the `update` methods to `update_with_event_cache` in the `latest_events` module. It frees the road to introduce `update_with_send_queue`. --- crates/matrix-sdk/src/latest_events/latest_event.rs | 5 +++-- crates/matrix-sdk/src/latest_events/mod.rs | 11 ++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index b2e5c9e4301..e1a0634bd4a 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -67,8 +67,9 @@ impl LatestEvent { self.value.subscribe().await } - /// Update the inner latest event value. - pub async fn update( + /// Update the inner latest event value, based on the event cache + /// (specifically with a [`RoomEventCache`]). + pub async fn update_with_event_cache( &mut self, room_event_cache: &RoomEventCache, power_levels: &Option<(&UserId, RoomPowerLevels)>, diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index d43ac9d51bb..db5903b436c 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -510,8 +510,9 @@ impl RoomLatestEvents { self.per_thread.get(thread_id) } - /// Update the latest events for the room and its threads. - async fn update(&mut self) { + /// Update the latest events for the room and its threads, based on the + /// event cache data. + async fn update_with_event_cache(&mut self) { // Get the power levels of the user for the current room if the `WeakRoom` is // still valid. // @@ -528,10 +529,10 @@ impl RoomLatestEvents { None => None, }; - self.for_the_room.update(&self.room_event_cache, &power_levels).await; + self.for_the_room.update_with_event_cache(&self.room_event_cache, &power_levels).await; for latest_event in self.per_thread.values_mut() { - latest_event.update(&self.room_event_cache, &power_levels).await; + latest_event.update_with_event_cache(&self.room_event_cache, &power_levels).await; } } } @@ -682,7 +683,7 @@ async fn compute_latest_events( let mut rooms = registered_rooms.rooms.write().await; if let Some(room_latest_events) = rooms.get_mut(room_id) { - room_latest_events.update().await; + room_latest_events.update_with_event_cache().await; } else { error!(?room_id, "Failed to find the room"); From 90ada2bdaf7b885eb599d8903add215677119b3b Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 4 Aug 2025 16:31:47 +0200 Subject: [PATCH 04/17] feat(sdk): `compute_latest_events` broadcasts the update to `LatestEvent`. This patch updates `compute_latest_events` to broadcast a `RoomSendQueueUpdate` onto `LatestEvent`. It introduces the new `update_with_send_queue` method. --- .../src/latest_events/latest_event.rs | 8 +++++++- crates/matrix-sdk/src/latest_events/mod.rs | 20 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index e1a0634bd4a..2d3424bdaad 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -31,7 +31,7 @@ use ruma::{ }; use tracing::warn; -use crate::{event_cache::RoomEventCache, room::WeakRoom}; +use crate::{event_cache::RoomEventCache, room::WeakRoom, send_queue::RoomSendQueueUpdate}; /// The latest event of a room or a thread. /// @@ -84,6 +84,12 @@ impl LatestEvent { self.value.set(new_value).await; } + + /// Update the inner latest event value, based on the send queue + /// (specifically with a [`RoomSendQueueUpdate`]). + pub async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) { + todo!() + } } /// A latest event value! diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index db5903b436c..e756571e28d 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -535,6 +535,16 @@ impl RoomLatestEvents { latest_event.update_with_event_cache(&self.room_event_cache, &power_levels).await; } } + + /// Update the latest events for the room and its threads, based on the + /// send queue update. + async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) { + self.for_the_room.update_with_send_queue(send_queue_update).await; + + for latest_event in self.per_thread.values_mut() { + latest_event.update_with_send_queue(send_queue_update).await; + } + } } /// The task responsible to listen to the [`EventCache`] and the [`SendQueue`]. @@ -692,9 +702,15 @@ async fn compute_latest_events( } LatestEventQueueUpdate::SendQueue { room_id, update } => { - // let mut rooms = registered_rooms.rooms.write().await; + let mut rooms = registered_rooms.rooms.write().await; - todo!() + if let Some(room_latest_events) = rooms.get_mut(room_id) { + room_latest_events.update_with_send_queue(update).await; + } else { + error!(?room_id, "Failed to find the room"); + + continue; + } } } } From 7cfa34326aa0d34ed4b2f7a576694f244a240db0 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Aug 2025 10:38:01 +0200 Subject: [PATCH 05/17] chore(sdk): Rename and split a couple of types in `latest_event`. This patch splits the `LatestEventValue` type into `LatestEventValue` + `LatestEventKind`. Basically, all variants in `LatestEventValue` are moved inside the new `LatestEventKind` enum. `LatestEventValue` keeps `None`, and see the new `Remote`, `LocalIsSending` and `LocalIsWedged` variants. This patch also extracts the message-like handling of `find_and_map` (now renamed `find_and_map_timeline_event`) into its own function: `find_and_map_any_message_like_event_content`. This is going to be handful for the send queue part. --- .../src/latest_events/latest_event.rs | 350 ++++++++++-------- crates/matrix-sdk/src/latest_events/mod.rs | 20 +- 2 files changed, 201 insertions(+), 169 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 2d3424bdaad..c6a1445d1bd 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::iter::once; + use eyeball::{AsyncLock, SharedObservable, Subscriber}; use matrix_sdk_base::event_cache::Event; use ruma::{ events::{ - call::{invite::SyncCallInviteEvent, notify::SyncCallNotifyEvent}, - poll::unstable_start::SyncUnstablePollStartEvent, + call::{invite::CallInviteEventContent, notify::CallNotifyEventContent}, + poll::unstable_start::UnstablePollStartEventContent, relation::RelationType, room::{ - member::{MembershipState, SyncRoomMemberEvent}, - message::{MessageType, SyncRoomMessageEvent}, + member::{MembershipState, RoomMemberEventContent}, + message::{MessageType, RoomMessageEventContent}, power_levels::RoomPowerLevels, }, - sticker::SyncStickerEvent, - AnySyncMessageLikeEvent, AnySyncStateEvent, AnySyncTimelineEvent, + sticker::StickerEventContent, + AnyMessageLikeEventContent, AnySyncStateEvent, AnySyncTimelineEvent, SyncStateEvent, }, - EventId, OwnedEventId, OwnedRoomId, RoomId, UserId, + EventId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, TransactionId, UserId, }; use tracing::warn; @@ -39,11 +41,13 @@ use crate::{event_cache::RoomEventCache, room::WeakRoom, send_queue::RoomSendQue #[derive(Debug)] pub(super) struct LatestEvent { /// The room owning this latest event. - room_id: OwnedRoomId, + _room_id: OwnedRoomId, + /// The thread (if any) owning this latest event. - thread_id: Option, + _thread_id: Option, + /// The latest event value. - value: SharedObservable, + current_value: SharedObservable, } impl LatestEvent { @@ -54,17 +58,17 @@ impl LatestEvent { weak_room: &WeakRoom, ) -> Self { Self { - room_id: room_id.to_owned(), - thread_id: thread_id.map(ToOwned::to_owned), - value: SharedObservable::new_async( - LatestEventValue::new(room_id, thread_id, room_event_cache, weak_room).await, + _room_id: room_id.to_owned(), + _thread_id: thread_id.map(ToOwned::to_owned), + current_value: SharedObservable::new_async( + LatestEventValue::new_remote(room_event_cache, weak_room).await, ), } } /// Return a [`Subscriber`] to new values. pub async fn subscribe(&self) -> Subscriber { - self.value.subscribe().await + self.current_value.subscribe().await } /// Update the inner latest event value, based on the event cache @@ -74,15 +78,10 @@ impl LatestEvent { room_event_cache: &RoomEventCache, power_levels: &Option<(&UserId, RoomPowerLevels)>, ) { - let new_value = LatestEventValue::new_with_power_levels( - &self.room_id, - self.thread_id.as_deref(), - room_event_cache, - power_levels, - ) - .await; - - self.value.set(new_value).await; + let new_value = + LatestEventValue::new_remote_with_power_levels(room_event_cache, power_levels).await; + + self.update(new_value).await; } /// Update the inner latest event value, based on the send queue @@ -90,6 +89,16 @@ impl LatestEvent { pub async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) { todo!() } + + /// Update [`Self::current_value`] if and only if the `new_value` is not + /// [`LatestEventValue::None`]. + async fn update(&mut self, new_value: LatestEventValue) { + if let LatestEventValue::None = new_value { + // Do not update to a `None` value. + } else { + self.current_value.set(new_value).await; + } + } } /// A latest event value! @@ -99,33 +108,20 @@ pub enum LatestEventValue { #[default] None, - /// A `m.room.message` event. - RoomMessage(SyncRoomMessageEvent), - - /// A `m.sticker` event. - Sticker(SyncStickerEvent), + /// The latest event represents a remote event. + Remote(LatestEventKind), - /// An `org.matrix.msc3381.poll.start` event. - Poll(SyncUnstablePollStartEvent), + /// The latest event represents a local event that is sending. + LocalIsSending(LatestEventKind), - /// A `m.call.invite` event. - CallInvite(SyncCallInviteEvent), - - /// A `m.call.notify` event. - CallNotify(SyncCallNotifyEvent), - - /// A `m.room.member` event, more precisely a knock membership change that - /// can be handled by the current user. - KnockedStateEvent(SyncRoomMemberEvent), + /// The latest event represents a local event that is wedged, either because + /// a previous local event, or this local event cannot be sent. + LocalIsWedged(LatestEventKind), } impl LatestEventValue { - async fn new( - room_id: &RoomId, - thread_id: Option<&EventId>, - room_event_cache: &RoomEventCache, - weak_room: &WeakRoom, - ) -> Self { + /// Create a new [`LatestEventValue::Remote`]. + async fn new_remote(room_event_cache: &RoomEventCache, weak_room: &WeakRoom) -> Self { // Get the power levels of the user for the current room if the `WeakRoom` is // still valid. let room = weak_room.get(); @@ -139,26 +135,50 @@ impl LatestEventValue { None => None, }; - Self::new_with_power_levels(room_id, thread_id, room_event_cache, &power_levels).await + Self::new_remote_with_power_levels(room_event_cache, &power_levels).await } - async fn new_with_power_levels( - _room_id: &RoomId, - _thread_id: Option<&EventId>, + /// Create a new [`LatestEventValue::Remote`] based on existing power + /// levels. + async fn new_remote_with_power_levels( room_event_cache: &RoomEventCache, power_levels: &Option<(&UserId, RoomPowerLevels)>, ) -> Self { room_event_cache - .rfind_map_event_in_memory_by(|event| find_and_map(event, power_levels)) + .rfind_map_event_in_memory_by(|event| find_and_map_timeline_event(event, power_levels)) .await + .map(Self::Remote) .unwrap_or_default() } } -fn find_and_map( +/// A latest event value! +#[derive(Debug, Clone)] +pub enum LatestEventKind { + /// A `m.room.message` event. + RoomMessage(RoomMessageEventContent), + + /// A `m.sticker` event. + Sticker(StickerEventContent), + + /// An `org.matrix.msc3381.poll.start` event. + Poll(UnstablePollStartEventContent), + + /// A `m.call.invite` event. + CallInvite(CallInviteEventContent), + + /// A `m.call.notify` event. + CallNotify(CallNotifyEventContent), + + /// A `m.room.member` event, more precisely a knock membership change that + /// can be handled by the current user. + KnockedStateEvent(Option), +} + +fn find_and_map_timeline_event( event: &Event, power_levels: &Option<(&UserId, RoomPowerLevels)>, -) -> Option { +) -> Option { // Cast the event into an `AnySyncTimelineEvent`. If deserializing fails, we // ignore the event. let Some(event) = event.raw().deserialize().ok() else { @@ -168,56 +188,20 @@ fn find_and_map( }; match event { - AnySyncTimelineEvent::MessageLike(message_like_event) => match message_like_event { - AnySyncMessageLikeEvent::RoomMessage(message) => { - if let Some(original_message) = message.as_original() { - // Don't show incoming verification requests. - if let MessageType::VerificationRequest(_) = original_message.content.msgtype { - return None; - } - - // Check if this is a replacement for another message. If it is, ignore it. - let is_replacement = - original_message.content.relates_to.as_ref().is_some_and(|relates_to| { - if let Some(relation_type) = relates_to.rel_type() { - relation_type == RelationType::Replacement - } else { - false - } - }); - - if is_replacement { - None - } else { - Some(LatestEventValue::RoomMessage(message)) - } - } else { - Some(LatestEventValue::RoomMessage(message)) + AnySyncTimelineEvent::MessageLike(message_like_event) => { + match message_like_event.original_content() { + Some(any_message_like_event_content) => { + find_and_map_any_message_like_event_content(any_message_like_event_content) } - } - - AnySyncMessageLikeEvent::UnstablePollStart(poll) => Some(LatestEventValue::Poll(poll)), - AnySyncMessageLikeEvent::CallInvite(invite) => { - Some(LatestEventValue::CallInvite(invite)) + // The event has been redacted. + None => todo!("what to do with a redacted message-like event?"), } + } - AnySyncMessageLikeEvent::CallNotify(notify) => { - Some(LatestEventValue::CallNotify(notify)) - } - - AnySyncMessageLikeEvent::Sticker(sticker) => Some(LatestEventValue::Sticker(sticker)), - - // Encrypted events are not suitable. - AnySyncMessageLikeEvent::RoomEncrypted(_) => None, - - // Everything else is considered not suitable. - _ => None, - }, - - // We don't currently support most state events + // We don't currently support most state events… AnySyncTimelineEvent::State(state) => { - // But we make an exception for knocked state events *if* the current user + // … but we make an exception for knocked state events *if* the current user // can either accept or decline them. if let AnySyncStateEvent::RoomMember(member) = state { if matches!(member.membership(), MembershipState::Knock) { @@ -232,7 +216,10 @@ fn find_and_map( // The current user can act on the knock changes, so they should be // displayed if can_accept_or_decline_knocks { - return Some(LatestEventValue::KnockedStateEvent(member)); + return Some(LatestEventKind::KnockedStateEvent(match member { + SyncStateEvent::Original(member) => Some(member.content), + SyncStateEvent::Redacted(_) => None, + })); } } } @@ -242,18 +229,58 @@ fn find_and_map( } } +fn find_and_map_any_message_like_event_content( + event: AnyMessageLikeEventContent, +) -> Option { + match event { + AnyMessageLikeEventContent::RoomMessage(message) => { + // Don't show incoming verification requests. + if let MessageType::VerificationRequest(_) = message.msgtype { + return None; + } + + // Check if this is a replacement for another message. If it is, ignore + // it. + let is_replacement = message.relates_to.as_ref().is_some_and(|relates_to| { + if let Some(relation_type) = relates_to.rel_type() { + relation_type == RelationType::Replacement + } else { + false + } + }); + + if is_replacement { + None + } else { + Some(LatestEventKind::RoomMessage(message)) + } + } + + AnyMessageLikeEventContent::UnstablePollStart(poll) => Some(LatestEventKind::Poll(poll)), + + AnyMessageLikeEventContent::CallInvite(invite) => Some(LatestEventKind::CallInvite(invite)), + + AnyMessageLikeEventContent::CallNotify(notify) => Some(LatestEventKind::CallNotify(notify)), + + AnyMessageLikeEventContent::Sticker(sticker) => Some(LatestEventKind::Sticker(sticker)), + + // Encrypted events are not suitable. + AnyMessageLikeEventContent::RoomEncrypted(_) => None, + + // Everything else is considered not suitable. + _ => None, + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; use matrix_sdk_test::event_factory::EventFactory; - use ruma::{ - event_id, events::room::power_levels::RoomPowerLevelsSource, - room_version_rules::AuthorizationRules, user_id, - }; + use ruma::{event_id, user_id}; - use super::{find_and_map, LatestEventValue}; + use super::{find_and_map_timeline_event, LatestEventKind}; - macro_rules! assert_latest_event_value { + macro_rules! assert_latest_event_kind { ( with | $event_factory:ident | $event_builder:block it produces $match:pat ) => { let user_id = user_id!("@mnt_io:matrix.org"); @@ -263,23 +290,24 @@ mod tests { $event_builder }; - assert_matches!(find_and_map(&event, &None), $match); + assert_matches!(find_and_map_timeline_event(&event, &None), $match); }; } #[test] - fn test_latest_event_value_room_message() { - assert_latest_event_value!( + fn test_latest_event_kind_room_message() { + assert_latest_event_kind!( with |event_factory| { event_factory.text_msg("hello").into_event() } - it produces Some(LatestEventValue::RoomMessage(_)) + it produces Some(LatestEventKind::RoomMessage(_)) ); } #[test] - fn test_latest_event_value_room_message_redacted() { - assert_latest_event_value!( + #[ignore] + fn test_latest_event_kind_room_message_redacted() { + assert_latest_event_kind!( with |event_factory| { event_factory .redacted( @@ -288,13 +316,13 @@ mod tests { ) .into_event() } - it produces Some(LatestEventValue::RoomMessage(_)) + it produces Some(LatestEventKind::RoomMessage(_)) ); } #[test] - fn test_latest_event_value_room_message_replacement() { - assert_latest_event_value!( + fn test_latest_event_kind_room_message_replacement() { + assert_latest_event_kind!( with |event_factory| { event_factory .text_msg("bonjour") @@ -309,8 +337,8 @@ mod tests { } #[test] - fn test_latest_event_value_poll() { - assert_latest_event_value!( + fn test_latest_event_kind_poll() { + assert_latest_event_kind!( with |event_factory| { event_factory .poll_start( @@ -320,13 +348,13 @@ mod tests { ) .into_event() } - it produces Some(LatestEventValue::Poll(_)) + it produces Some(LatestEventKind::Poll(_)) ); } #[test] - fn test_latest_event_value_call_invite() { - assert_latest_event_value!( + fn test_latest_event_kind_call_invite() { + assert_latest_event_kind!( with |event_factory| { event_factory .call_invite( @@ -337,13 +365,13 @@ mod tests { ) .into_event() } - it produces Some(LatestEventValue::CallInvite(_)) + it produces Some(LatestEventKind::CallInvite(_)) ); } #[test] - fn test_latest_event_value_call_notify() { - assert_latest_event_value!( + fn test_latest_event_kind_call_notify() { + assert_latest_event_kind!( with |event_factory| { event_factory .call_notify( @@ -354,13 +382,13 @@ mod tests { ) .into_event() } - it produces Some(LatestEventValue::CallNotify(_)) + it produces Some(LatestEventKind::CallNotify(_)) ); } #[test] - fn test_latest_event_value_sticker() { - assert_latest_event_value!( + fn test_latest_event_kind_sticker() { + assert_latest_event_kind!( with |event_factory| { event_factory .sticker( @@ -370,13 +398,13 @@ mod tests { ) .into_event() } - it produces Some(LatestEventValue::Sticker(_)) + it produces Some(LatestEventKind::Sticker(_)) ); } #[test] - fn test_latest_event_value_encrypted_room_message() { - assert_latest_event_value!( + fn test_latest_event_kind_encrypted_room_message() { + assert_latest_event_kind!( with |event_factory| { event_factory .event(ruma::events::room::encrypted::RoomEncryptedEventContent::new( @@ -398,9 +426,9 @@ mod tests { } #[test] - fn test_latest_event_value_reaction() { + fn test_latest_event_kind_reaction() { // Take a random message-like event. - assert_latest_event_value!( + assert_latest_event_kind!( with |event_factory| { event_factory .reaction(event_id!("$ev0"), "+1") @@ -411,8 +439,8 @@ mod tests { } #[test] - fn test_latest_event_state_event() { - assert_latest_event_value!( + fn test_latest_event_kind_state_event() { + assert_latest_event_kind!( with |event_factory| { event_factory .room_topic("new room topic") @@ -423,8 +451,8 @@ mod tests { } #[test] - fn test_latest_event_knocked_state_event_without_power_levels() { - assert_latest_event_value!( + fn test_latest_event_kind_knocked_state_event_without_power_levels() { + assert_latest_event_kind!( with |event_factory| { event_factory .member(user_id!("@other_mnt_io:server.name")) @@ -436,16 +464,20 @@ mod tests { } #[test] - fn test_latest_event_knocked_state_event_with_power_levels() { - use ruma::events::room::power_levels::RoomPowerLevels; + fn test_latest_event_kind_knocked_state_event_with_power_levels() { + use ruma::{ + events::room::{ + member::MembershipState, + power_levels::{RoomPowerLevels, RoomPowerLevelsSource}, + }, + room_version_rules::AuthorizationRules, + }; let user_id = user_id!("@mnt_io:matrix.org"); let other_user_id = user_id!("@other_mnt_io:server.name"); let event_factory = EventFactory::new().sender(user_id); - let event = event_factory - .member(other_user_id) - .membership(ruma::events::room::member::MembershipState::Knock) - .into_event(); + let event = + event_factory.member(other_user_id).membership(MembershipState::Knock).into_event(); let mut room_power_levels = RoomPowerLevels::new(RoomPowerLevelsSource::None, &AuthorizationRules::V1, []); @@ -457,7 +489,7 @@ mod tests { room_power_levels.invite = 10.into(); room_power_levels.kick = 10.into(); assert_matches!( - find_and_map(&event, &Some((user_id, room_power_levels))), + find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), None, "cannot accept, cannot decline", ); @@ -469,8 +501,8 @@ mod tests { room_power_levels.invite = 0.into(); room_power_levels.kick = 10.into(); assert_matches!( - find_and_map(&event, &Some((user_id, room_power_levels))), - Some(LatestEventValue::KnockedStateEvent(_)), + find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), + Some(LatestEventKind::KnockedStateEvent(_)), "can accept, cannot decline", ); } @@ -481,8 +513,8 @@ mod tests { room_power_levels.invite = 10.into(); room_power_levels.kick = 0.into(); assert_matches!( - find_and_map(&event, &Some((user_id, room_power_levels))), - Some(LatestEventValue::KnockedStateEvent(_)), + find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), + Some(LatestEventKind::KnockedStateEvent(_)), "cannot accept, can decline", ); } @@ -492,25 +524,27 @@ mod tests { room_power_levels.invite = 0.into(); room_power_levels.kick = 0.into(); assert_matches!( - find_and_map(&event, &Some((user_id, room_power_levels))), - Some(LatestEventValue::KnockedStateEvent(_)), + find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), + Some(LatestEventKind::KnockedStateEvent(_)), "can accept, can decline", ); } } #[test] - fn test_latest_event_value_room_message_verification_request() { - assert_latest_event_value!( + fn test_latest_event_kind_room_message_verification_request() { + use ruma::{events::room::message, OwnedDeviceId}; + + assert_latest_event_kind!( with |event_factory| { event_factory .event( - ruma::events::room::message::RoomMessageEventContent::new( - ruma::events::room::message::MessageType::VerificationRequest( - ruma::events::room::message::KeyVerificationRequestEventContent::new( + message::RoomMessageEventContent::new( + message::MessageType::VerificationRequest( + message::KeyVerificationRequestEventContent::new( "body".to_owned(), vec![], - ruma::OwnedDeviceId::from("device_id"), + OwnedDeviceId::from("device_id"), user_id!("@user:server.name").to_owned(), ) ) @@ -529,11 +563,11 @@ mod tests_non_wasm { use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{event_id, room_id, user_id}; - use super::LatestEventValue; + use super::{LatestEventKind, LatestEventValue}; use crate::test_utils::mocks::MatrixMockServer; #[async_test] - async fn test_latest_event_value_is_scanning_event_backwards_from_event_cache() { + async fn test_latest_event_value_remote_is_scanning_event_backwards_from_event_cache() { use matrix_sdk_base::{ linked_chunk::{ChunkIdentifier, Position, Update}, RoomState, @@ -597,12 +631,12 @@ mod tests_non_wasm { let weak_room = WeakRoom::new(WeakClient::from_client(&client), room_id.to_owned()); assert_matches!( - LatestEventValue::new(room_id, None, &room_event_cache, &weak_room).await, - LatestEventValue::RoomMessage(given_event) => { + LatestEventValue::new_remote(&room_event_cache, &weak_room).await, + LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { // We get `event_id_1` because `event_id_2` isn't a candidate, // and `event_id_0` hasn't been read yet (because events are // read backwards). - assert_eq!(given_event.event_id(), event_id_1); + assert_eq!(message_content.msgtype.body(), "world"); } ); } diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index e756571e28d..059bf23ec0f 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -59,7 +59,7 @@ pub use error::LatestEventsError; use eyeball::{AsyncLock, Subscriber}; use futures_util::FutureExt; use latest_event::LatestEvent; -pub use latest_event::LatestEventValue; +pub use latest_event::{LatestEventKind, LatestEventValue}; use matrix_sdk_common::executor::{spawn, AbortOnDrop, JoinHandleExt as _}; use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId}; use tokio::{ @@ -730,8 +730,9 @@ mod tests { use stream_assert::assert_pending; use super::{ - broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventValue, - RoomEventCacheGenericUpdate, RoomRegistration, RoomSendQueueUpdate, SendQueueUpdate, + broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventKind, + LatestEventValue, RoomEventCacheGenericUpdate, RoomRegistration, RoomSendQueueUpdate, + SendQueueUpdate, }; use crate::test_utils::mocks::MatrixMockServer; @@ -1251,8 +1252,8 @@ mod tests { // latest event! assert_matches!( latest_event_stream.get().await, - LatestEventValue::RoomMessage(event) => { - assert_eq!(event.event_id(), event_id_1); + LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { + assert_eq!(message_content.msgtype.body(), "world"); } ); @@ -1264,10 +1265,7 @@ mod tests { .sync_room( &client, JoinedRoomBuilder::new(&room_id).add_timeline_event( - event_factory - .text_msg("venez découvrir cette nouvelle raclette !") - .event_id(event_id_2) - .into_raw(), + event_factory.text_msg("raclette !").event_id(event_id_2).into_raw(), ), ) .await; @@ -1277,8 +1275,8 @@ mod tests { // `compute_latest_events` which has updated the latest event value. assert_matches!( latest_event_stream.next().await, - Some(LatestEventValue::RoomMessage(event)) => { - assert_eq!(event.event_id(), event_id_2); + Some(LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content))) => { + assert_eq!(message_content.msgtype.body(), "raclette !"); } ); } From 31f3243218174c534e49eb80c5e5e6eb997731f2 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Aug 2025 10:45:57 +0200 Subject: [PATCH 06/17] feat(sdk): Implement `LatestEvent::update_with_send_queue`. This patch implements `LatestEvent::update_with_send_queue`. It introduces an intermediate type, for the sake of clarity, `LatestEventValuesForLocalEvents`. The difficulty here is to keep a buffer of `LatestEventValue`s requested by the `SendQueue`. Why? Because we want the latest event value, but we only receive `RoomSendQueueUpdate`s, we can't iterate over local events in the `SendQueue` like we do for the `EventCache` to re-compute the latest event if a local event has been cancelled or updated. A particular care must also be applied when a local event is wedged: this local event and all its followings must be marked as wedged too, so that the `LatestEventValue` is `LocalIsWedged`. Same when the local event is unwedged. --- .../src/latest_events/latest_event.rs | 349 +++++++++++++++++- crates/matrix-sdk/src/latest_events/mod.rs | 24 +- 2 files changed, 369 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index c6a1445d1bd..09bbaf0d91c 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -46,6 +46,11 @@ pub(super) struct LatestEvent { /// The thread (if any) owning this latest event. _thread_id: Option, + /// A buffer of the current [`LatestEventValue`] computed for local events + /// seen by the send queue. See [`LatestEventValuesForLocalEvents`] to learn + /// more. + buffer_of_values_for_local_events: LatestEventValuesForLocalEvents, + /// The latest event value. current_value: SharedObservable, } @@ -60,6 +65,7 @@ impl LatestEvent { Self { _room_id: room_id.to_owned(), _thread_id: thread_id.map(ToOwned::to_owned), + buffer_of_values_for_local_events: LatestEventValuesForLocalEvents::new(), current_value: SharedObservable::new_async( LatestEventValue::new_remote(room_event_cache, weak_room).await, ), @@ -86,8 +92,21 @@ impl LatestEvent { /// Update the inner latest event value, based on the send queue /// (specifically with a [`RoomSendQueueUpdate`]). - pub async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) { - todo!() + pub async fn update_with_send_queue( + &mut self, + send_queue_update: &RoomSendQueueUpdate, + room_event_cache: &RoomEventCache, + power_levels: &Option<(&UserId, RoomPowerLevels)>, + ) { + let new_value = LatestEventValue::new_local( + send_queue_update, + &mut self.buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await; + + self.update(new_value).await; } /// Update [`Self::current_value`] if and only if the `new_value` is not @@ -150,6 +169,332 @@ impl LatestEventValue { .map(Self::Remote) .unwrap_or_default() } + + /// Create a new [`LatestEventValue::LocalIsSending`] or + /// [`LatestEventValue::LocalIsWedged`]. + async fn new_local( + send_queue_update: &RoomSendQueueUpdate, + buffer_of_values_for_local_events: &mut LatestEventValuesForLocalEvents, + room_event_cache: &RoomEventCache, + power_levels: &Option<(&UserId, RoomPowerLevels)>, + ) -> Self { + use crate::send_queue::{LocalEcho, LocalEchoContent}; + + match send_queue_update { + // A new local event is being sent. + // + // Let's create the `LatestEventValue` and push it in the buffer of values. + RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id, + content: local_echo_content, + }) => match local_echo_content { + LocalEchoContent::Event { serialized_event: content, .. } => { + if let Ok(content) = content.deserialize() { + if let Some(kind) = find_and_map_any_message_like_event_content(content) { + let value = Self::LocalIsSending(kind); + + buffer_of_values_for_local_events + .push(transaction_id.to_owned(), value.clone()); + + value + } else { + Self::None + } + } else { + Self::None + } + } + + LocalEchoContent::React { .. } => Self::None, + }, + + // A local event has been cancelled before being sent. + // + // Remove the calculated `LatestEventValue` from the buffer of values, and return the + // last `LatestEventValue` or calculate a new one. + RoomSendQueueUpdate::CancelledLocalEvent { transaction_id } => { + if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { + buffer_of_values_for_local_events.remove(position); + } + + Self::new_local_or_remote( + buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await + } + + // A local event has successfully been sent! + // + // Unwedge all wedged values after the one matching `transaction_id`. Indeed, if + // an event has been sent, it means the send queue is working, so if any value has been + // marked as wedged, it must be marked as unwedged. Then, remove the calculated + // `LatestEventValue` from the buffer of values. Finally, return the last + // `LatestEventValue` or calculate a new one. + RoomSendQueueUpdate::SentEvent { transaction_id, .. } => { + let position = buffer_of_values_for_local_events.unwedged_after(transaction_id); + + if let Some(position) = position { + buffer_of_values_for_local_events.remove(position); + } + + Self::new_local_or_remote( + buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await + } + + // A local event has been replaced by another one. + // + // Replace the latest event value matching `transaction_id` in the buffer if it exists + // (note: it should!), and return the last `LatestEventValue` or calculate a new one. + RoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, new_content: content } => { + if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { + if let Ok(content) = content.deserialize() { + if let Some(kind) = find_and_map_any_message_like_event_content(content) { + buffer_of_values_for_local_events.replace_kind(position, kind); + } + } else { + return Self::None; + } + } + + Self::new_local_or_remote( + buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await + } + + // An error has occurred. + // + // Mark the latest event value matching `transaction_id`, and all its following values, + // as wedged. + RoomSendQueueUpdate::SendError { transaction_id, .. } => { + buffer_of_values_for_local_events.wedged_from(transaction_id); + + Self::new_local_or_remote( + buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await + } + + // A local event has been unwedged and sending is being retried. + // + // Mark the latest event value matching `transaction_id`, and all its following values, + // as unwedged. + RoomSendQueueUpdate::RetryEvent { transaction_id } => { + buffer_of_values_for_local_events.unwedged_from(transaction_id); + + Self::new_local_or_remote( + buffer_of_values_for_local_events, + room_event_cache, + power_levels, + ) + .await + } + + // A media upload has made progress. + // + // Nothing to do here. + RoomSendQueueUpdate::MediaUpload { .. } => Self::None, + } + } + + /// Get the last [`LatestEventValue`] from the local latest event values if + /// any, or create a new [`LatestEventValue`] from the remote events. + /// + /// If the buffer of latest event values is not empty, let's return the last + /// one. Otherwise, it means we no longer have any local event: let's + /// fallback on remote event! + async fn new_local_or_remote( + buffer_of_values_for_local_events: &mut LatestEventValuesForLocalEvents, + room_event_cache: &RoomEventCache, + power_levels: &Option<(&UserId, RoomPowerLevels)>, + ) -> Self { + if let Some(value) = buffer_of_values_for_local_events.last() { + value.clone() + } else { + Self::new_remote_with_power_levels(room_event_cache, power_levels).await + } + } +} + +/// A buffer of the current [`LatestEventValue`] computed for local events +/// seen by the send queue. It is used by +/// [`LatestEvent::buffer_of_values_for_local_events`]. +/// +/// The system does only receive [`RoomSendQueueUpdate`]s. It's not designed to +/// iterate over local events in the send queue when a local event is changed +/// (cancelled, or updated for example). That's why we keep our own buffer here. +/// Imagine the system receives 4 [`RoomSendQueueUpdate`]: +/// +/// 1. [`RoomSendQueueUpdate::NewLocalEvent`]: new local event, +/// 2. [`RoomSendQueueUpdate::NewLocalEvent`]: new local event, +/// 3. [`RoomSendQueueUpdate::ReplacedLocalEvent`]: replaced the first local +/// event, +/// 4. [`RoomSendQueueUpdate::CancelledLocalEvent`]: cancelled the second local +/// event. +/// +/// `NewLocalEvent`s will trigger the computation of new +/// `LatestEventValue`s, but `CancelledLocalEvent` for example doesn't hold +/// any information to compute a new `LatestEventValue`, so we need to +/// remember the previous values, until the local events are sent and +/// removed from this buffer. +/// +/// Another reason why we need a buffer is to handle wedged local event. Imagine +/// the system receives 3 [`RoomSendQueueUpdate`]: +/// +/// 1. [`RoomSendQueueUpdate::NewLocalEvent`]: new local event, +/// 2. [`RoomSendQueueUpdate::NewLocalEvent`]: new local event, +/// 3. [`RoomSendQueueUpdate::SendError`]: the first local event has failed to +/// be sent. +/// +/// Because a `SendError` is received (targeting the first `NewLocalEvent`), the +/// send queue is stopped. However, the `LatestEventValue` targets the second +/// `NewLocalEvent`. The system must consider that when a local event is wedged, +/// all the following local events must also be marked as wedged. And vice +/// versa, when the send queue is able to send an event again, all the following +/// local events must be marked as unwedged. +/// +/// This type isolates a couple of methods designed to manage these specific +/// behaviours. +#[derive(Debug)] +struct LatestEventValuesForLocalEvents { + buffer: Vec<(OwnedTransactionId, LatestEventValue)>, +} + +impl LatestEventValuesForLocalEvents { + /// Create a new [`LatestEventValuesForLocalEvents`]. + fn new() -> Self { + Self { buffer: Vec::with_capacity(2) } + } + + /// Get the last [`LatestEventValue`]. + fn last(&self) -> Option<&LatestEventValue> { + self.buffer.last().map(|(_, value)| value) + } + + /// Find the position of the [`LatestEventValue`] matching `transaction_id`. + fn position(&self, transaction_id: &TransactionId) -> Option { + self.buffer + .iter() + .position(|(transaction_id_candidate, _)| transaction_id == transaction_id_candidate) + } + + /// Push a new [`LatestEventValue`]. + /// + /// # Panics + /// + /// Panics if `value` is not of kind [`LatestEventValue::LocalIsSending`] or + /// [`LatestEventValue::LocalIsWedged`]. + fn push(&mut self, transaction_id: OwnedTransactionId, value: LatestEventValue) { + assert!( + matches!( + value, + LatestEventValue::LocalIsSending(_) | LatestEventValue::LocalIsWedged(_) + ), + "`value` must be either `LocalIsSending` or `LocalIsWedged`" + ); + + self.buffer.push((transaction_id, value)); + } + + /// Replace the [`LatestEventKind`] of the [`LatestEventValue`] at position + /// `position`. + /// + /// # Panics + /// + /// Panics if: + /// - `position` is strictly greater than buffer's length, + /// - the [`LatestEventValue`] is not of kind + /// [`LatestEventValue::LocalIsSending`] or + /// [`LatestEventValue::LocalIsWedged`]. + fn replace_kind(&mut self, position: usize, new_kind: LatestEventKind) { + let (_, value) = self.buffer.get_mut(position).expect("`position` must be valid"); + + match value { + LatestEventValue::LocalIsSending(kind) => *kind = new_kind, + LatestEventValue::LocalIsWedged(kind) => *kind = new_kind, + _ => panic!("`value` must be either `LocalIsSending` or `LocalIsWedged`"), + } + } + + /// Remove the [`LatestEventValue`] at position `position`. + /// + /// # Panics + /// + /// Panics if `position` is strictly greater than buffer's length. + fn remove(&mut self, position: usize) -> (OwnedTransactionId, LatestEventValue) { + self.buffer.remove(position) + } + + /// Mark the `LatestEventValue` matching `transaction_id`, and all the + /// following values, as wedged. + fn wedged_from(&mut self, transaction_id: &TransactionId) { + let mut values = self.buffer.iter_mut(); + + if let Some(first_value_to_wedge) = values + .by_ref() + .find(|(transaction_id_candidate, _)| transaction_id == transaction_id_candidate) + { + // Iterate over the found value and the following ones. + for (_, value_to_wedge) in once(first_value_to_wedge).chain(values) { + if let LatestEventValue::LocalIsSending(kind) = value_to_wedge { + *value_to_wedge = LatestEventValue::LocalIsWedged(kind.clone()); + } + } + } + } + + /// Mark the `LatestEventValue` matching `transaction_id`, and all the + /// following values, as unwedged. + fn unwedged_from(&mut self, transaction_id: &TransactionId) { + let mut values = self.buffer.iter_mut(); + + if let Some(first_value_to_unwedge) = values + .by_ref() + .find(|(transaction_id_candidate, _)| transaction_id == transaction_id_candidate) + { + // Iterate over the found value and the following ones. + for (_, value_to_unwedge) in once(first_value_to_unwedge).chain(values) { + if let LatestEventValue::LocalIsWedged(kind) = value_to_unwedge { + *value_to_unwedge = LatestEventValue::LocalIsSending(kind.clone()); + } + } + } + } + + /// Mark all the following values after the `LatestEventValue` matching + /// `transaction_id` as unwedged. + /// + /// Note that contrary to [`Self::unwedged_from`], the `LatestEventValue` is + /// untouched. However, its position is returned (if any). + fn unwedged_after(&mut self, transaction_id: &TransactionId) -> Option { + let mut values = self.buffer.iter_mut(); + + if let Some(position) = values + .by_ref() + .position(|(transaction_id_candidate, _)| transaction_id == transaction_id_candidate) + { + // Iterate over all values after the found one. + for (_, value_to_unwedge) in values { + if let LatestEventValue::LocalIsWedged(kind) = value_to_unwedge { + *value_to_unwedge = LatestEventValue::LocalIsSending(kind.clone()); + } + } + + Some(position) + } else { + None + } + } } /// A latest event value! diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 059bf23ec0f..85a6998ba8d 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -539,10 +539,30 @@ impl RoomLatestEvents { /// Update the latest events for the room and its threads, based on the /// send queue update. async fn update_with_send_queue(&mut self, send_queue_update: &RoomSendQueueUpdate) { - self.for_the_room.update_with_send_queue(send_queue_update).await; + // Get the power levels of the user for the current room if the `WeakRoom` is + // still valid. + // + // Get it once for all the updates of all the latest events for this room (be + // the room and its threads). + let room = self.weak_room.get(); + let power_levels = match &room { + Some(room) => { + let power_levels = room.power_levels().await.ok(); + + Some(room.own_user_id()).zip(power_levels) + } + + None => None, + }; + + self.for_the_room + .update_with_send_queue(send_queue_update, &self.room_event_cache, &power_levels) + .await; for latest_event in self.per_thread.values_mut() { - latest_event.update_with_send_queue(send_queue_update).await; + latest_event + .update_with_send_queue(send_queue_update, &self.room_event_cache, &power_levels) + .await; } } } From 0e8d6bac0fa3002c2d39ef6cafc7bc19cbbea61f Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 11 Aug 2025 12:06:52 +0200 Subject: [PATCH 07/17] test(sdk): Write tests for `LatestEvent` and `SendQueue`. --- .../src/latest_events/latest_event.rs | 947 +++++++++++++++++- crates/matrix-sdk/src/latest_events/mod.rs | 4 +- crates/matrix-sdk/src/send_queue/mod.rs | 12 +- 3 files changed, 929 insertions(+), 34 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 09bbaf0d91c..7fca5da8573 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -618,12 +618,12 @@ fn find_and_map_any_message_like_event_content( } #[cfg(test)] -mod tests { +mod tests_latest_event_kind { use assert_matches::assert_matches; use matrix_sdk_test::event_factory::EventFactory; use ruma::{event_id, user_id}; - use super::{find_and_map_timeline_event, LatestEventKind}; + use super::{find_and_map_timeline_event, LatestEventKind, RoomMessageEventContent}; macro_rules! assert_latest_event_kind { ( with | $event_factory:ident | $event_builder:block @@ -640,7 +640,7 @@ mod tests { } #[test] - fn test_latest_event_kind_room_message() { + fn test_room_message() { assert_latest_event_kind!( with |event_factory| { event_factory.text_msg("hello").into_event() @@ -651,7 +651,7 @@ mod tests { #[test] #[ignore] - fn test_latest_event_kind_room_message_redacted() { + fn test_room_message_redacted() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -666,14 +666,14 @@ mod tests { } #[test] - fn test_latest_event_kind_room_message_replacement() { + fn test_room_message_replacement() { assert_latest_event_kind!( with |event_factory| { event_factory .text_msg("bonjour") .edit( event_id!("$ev0"), - ruma::events::room::message::RoomMessageEventContent::text_plain("hello").into() + RoomMessageEventContent::text_plain("hello").into() ) .into_event() } @@ -682,7 +682,7 @@ mod tests { } #[test] - fn test_latest_event_kind_poll() { + fn test_poll() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -698,7 +698,7 @@ mod tests { } #[test] - fn test_latest_event_kind_call_invite() { + fn test_call_invite() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -715,7 +715,7 @@ mod tests { } #[test] - fn test_latest_event_kind_call_notify() { + fn test_call_notify() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -732,7 +732,7 @@ mod tests { } #[test] - fn test_latest_event_kind_sticker() { + fn test_sticker() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -748,7 +748,7 @@ mod tests { } #[test] - fn test_latest_event_kind_encrypted_room_message() { + fn test_encrypted_room_message() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -771,7 +771,7 @@ mod tests { } #[test] - fn test_latest_event_kind_reaction() { + fn test_reaction() { // Take a random message-like event. assert_latest_event_kind!( with |event_factory| { @@ -784,7 +784,7 @@ mod tests { } #[test] - fn test_latest_event_kind_state_event() { + fn test_state_event() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -796,7 +796,7 @@ mod tests { } #[test] - fn test_latest_event_kind_knocked_state_event_without_power_levels() { + fn test_knocked_state_event_without_power_levels() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -809,7 +809,7 @@ mod tests { } #[test] - fn test_latest_event_kind_knocked_state_event_with_power_levels() { + fn test_knocked_state_event_with_power_levels() { use ruma::{ events::room::{ member::MembershipState, @@ -877,14 +877,14 @@ mod tests { } #[test] - fn test_latest_event_kind_room_message_verification_request() { + fn test_room_message_verification_request() { use ruma::{events::room::message, OwnedDeviceId}; assert_latest_event_kind!( with |event_factory| { event_factory .event( - message::RoomMessageEventContent::new( + RoomMessageEventContent::new( message::MessageType::VerificationRequest( message::KeyVerificationRequestEventContent::new( "body".to_owned(), @@ -902,24 +902,223 @@ mod tests { } } +#[cfg(test)] +mod tests_latest_event_values_for_local_events { + use assert_matches::assert_matches; + use ruma::OwnedTransactionId; + + use super::{ + LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, RoomMessageEventContent, + }; + + fn room_message(body: &str) -> LatestEventKind { + LatestEventKind::RoomMessage(RoomMessageEventContent::text_plain(body)) + } + + #[test] + fn test_last() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + assert!(buffer.last().is_none()); + + buffer.push( + OwnedTransactionId::from("txnid"), + LatestEventValue::LocalIsSending(room_message("tome")), + ); + + assert_matches!( + buffer.last(), + Some(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(_))) + ); + } + + #[test] + fn test_position() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id = OwnedTransactionId::from("txnid"); + + assert!(buffer.position(&transaction_id).is_none()); + + buffer.push( + transaction_id.clone(), + LatestEventValue::LocalIsSending(room_message("raclette")), + ); + buffer.push( + OwnedTransactionId::from("othertxnid"), + LatestEventValue::LocalIsSending(room_message("tome")), + ); + + assert_eq!(buffer.position(&transaction_id), Some(0)); + } + + #[test] + #[should_panic] + fn test_push_none() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + buffer.push(OwnedTransactionId::from("txnid"), LatestEventValue::None); + } + + #[test] + #[should_panic] + fn test_push_remote() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + buffer.push( + OwnedTransactionId::from("txnid"), + LatestEventValue::Remote(room_message("tome")), + ); + } + + #[test] + fn test_push_local() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + buffer.push( + OwnedTransactionId::from("txnid0"), + LatestEventValue::LocalIsSending(room_message("tome")), + ); + buffer.push( + OwnedTransactionId::from("txnid1"), + LatestEventValue::LocalIsWedged(room_message("raclette")), + ); + + // no panic. + } + + #[test] + fn test_replace_kind() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + buffer.push( + OwnedTransactionId::from("txnid0"), + LatestEventValue::LocalIsSending(room_message("gruyère")), + ); + + buffer.replace_kind(0, room_message("comté")); + + assert_matches!( + buffer.last(), + Some(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(content))) => { + assert_eq!(content.body(), "comté"); + } + ); + } + + #[test] + fn test_remove() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + + buffer.push( + OwnedTransactionId::from("txnid"), + LatestEventValue::LocalIsSending(room_message("gryuère")), + ); + + assert!(buffer.last().is_some()); + + buffer.remove(0); + + assert!(buffer.last().is_none()); + } + + #[test] + fn test_wedged_from() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + let transaction_id_2 = OwnedTransactionId::from("txnid2"); + + buffer.push(transaction_id_0, LatestEventValue::LocalIsSending(room_message("gruyère"))); + buffer.push( + transaction_id_1.clone(), + LatestEventValue::LocalIsSending(room_message("brigand")), + ); + buffer.push(transaction_id_2, LatestEventValue::LocalIsSending(room_message("raclette"))); + + buffer.wedged_from(&transaction_id_1); + + assert_eq!(buffer.buffer.len(), 3); + assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsSending(_)); + assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsWedged(_)); + } + + #[test] + fn test_unwedged_from() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + let transaction_id_2 = OwnedTransactionId::from("txnid2"); + + buffer.push(transaction_id_0, LatestEventValue::LocalIsWedged(room_message("gruyère"))); + buffer.push( + transaction_id_1.clone(), + LatestEventValue::LocalIsWedged(room_message("brigand")), + ); + buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); + + buffer.unwedged_from(&transaction_id_1); + + assert_eq!(buffer.buffer.len(), 3); + assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsSending(_)); + assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsSending(_)); + } + + #[test] + fn test_unwedged_after() { + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + let transaction_id_2 = OwnedTransactionId::from("txnid2"); + + buffer.push(transaction_id_0, LatestEventValue::LocalIsWedged(room_message("gruyère"))); + buffer.push( + transaction_id_1.clone(), + LatestEventValue::LocalIsWedged(room_message("brigand")), + ); + buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); + + buffer.unwedged_after(&transaction_id_1); + + assert_eq!(buffer.buffer.len(), 3); + assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsSending(_)); + } +} + #[cfg(all(not(target_family = "wasm"), test))] -mod tests_non_wasm { +mod tests_latest_event_value_non_wasm { + use std::sync::Arc; + use assert_matches::assert_matches; + use matrix_sdk_base::{ + linked_chunk::{ChunkIdentifier, LinkedChunkId, Position, Update}, + store::SerializableEventContent, + RoomState, + }; use matrix_sdk_test::{async_test, event_factory::EventFactory}; - use ruma::{event_id, room_id, user_id}; + use ruma::{ + event_id, + events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, + room_id, user_id, MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedTransactionId, + }; - use super::{LatestEventKind, LatestEventValue}; - use crate::test_utils::mocks::MatrixMockServer; + use super::{ + LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, RoomEventCache, + RoomSendQueueUpdate, + }; + use crate::{ + client::WeakClient, + room::WeakRoom, + send_queue::{AbstractProgress, LocalEcho, LocalEchoContent, RoomSendQueue, SendHandle}, + test_utils::mocks::MatrixMockServer, + Client, Error, + }; #[async_test] - async fn test_latest_event_value_remote_is_scanning_event_backwards_from_event_cache() { - use matrix_sdk_base::{ - linked_chunk::{ChunkIdentifier, Position, Update}, - RoomState, - }; - - use crate::{client::WeakClient, room::WeakRoom}; - + async fn test_remote_is_scanning_event_backwards_from_event_cache() { let room_id = room_id!("!r0"); let user_id = user_id!("@mnt_io:matrix.org"); let event_factory = EventFactory::new().sender(user_id).room(room_id); @@ -942,7 +1141,7 @@ mod tests_non_wasm { .await .unwrap() .handle_linked_chunk_updates( - matrix_sdk_base::linked_chunk::LinkedChunkId::Room(room_id), + LinkedChunkId::Room(room_id), vec![ Update::NewItemsChunk { previous: None, @@ -981,7 +1180,693 @@ mod tests_non_wasm { // We get `event_id_1` because `event_id_2` isn't a candidate, // and `event_id_0` hasn't been read yet (because events are // read backwards). - assert_eq!(message_content.msgtype.body(), "world"); + assert_eq!(message_content.body(), "world"); + } + ); + } + + async fn local_prelude() -> (Client, OwnedRoomId, RoomSendQueue, RoomEventCache) { + let room_id = room_id!("!r0").to_owned(); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + client.base_client().get_or_create_room(&room_id, RoomState::Joined); + let room = client.get_room(&room_id).unwrap(); + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(&room_id).await.unwrap(); + + let send_queue = client.send_queue(); + let room_send_queue = send_queue.for_room(room); + + (client, room_id, room_send_queue, room_event_cache) + } + + fn new_local_echo_content( + room_send_queue: &RoomSendQueue, + transaction_id: &OwnedTransactionId, + body: &str, + ) -> LocalEchoContent { + LocalEchoContent::Event { + serialized_event: SerializableEventContent::new( + &AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain(body)), + ) + .unwrap(), + send_handle: SendHandle::new( + room_send_queue.clone(), + transaction_id.clone(), + MilliSecondsSinceUnixEpoch::now(), + ), + send_error: None, + } + } + + #[async_test] + async fn test_local_new_local_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + + // Receiving one `NewLocalEvent`. + { + let transaction_id = OwnedTransactionId::from("txnid0"); + let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { transaction_id, content }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(message_content)) => { + assert_eq!(message_content.body(), "A"); + } + ); + } + + // Receiving another `NewLocalEvent`, ensuring it's pushed back in the buffer. + { + let transaction_id = OwnedTransactionId::from("txnid1"); + let content = new_local_echo_content(&room_send_queue, &transaction_id, "B"); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { transaction_id, content }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + + assert_eq!(buffer.buffer.len(), 2); + assert_matches!( + &buffer.buffer[0].1, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + assert_matches!( + &buffer.buffer[1].1, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + + #[async_test] + async fn test_local_cancelled_local_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + let transaction_id_2 = OwnedTransactionId::from("txnid2"); + + // Receiving three `NewLocalEvent`s. + { + for (transaction_id, body) in + [(&transaction_id_0, "A"), (&transaction_id_1, "B"), (&transaction_id_2, "C")] + { + let content = new_local_echo_content(&room_send_queue, transaction_id, body); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), body); + } + ); + } + + assert_eq!(buffer.buffer.len(), 3); + } + + // Receiving a `CancelledLocalEvent` targeting the second event. The + // `LatestEventValue` must not change. + { + let update = RoomSendQueueUpdate::CancelledLocalEvent { + transaction_id: transaction_id_1.clone(), + }; + + // The `LatestEventValue` hasn't changed, it still matches the latest local + // event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "C"); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `CancelledLocalEvent` targeting the second (so the last) event. + // The `LatestEventValue` must point to the first local event. + { + let update = RoomSendQueueUpdate::CancelledLocalEvent { + transaction_id: transaction_id_2.clone(), + }; + + // The `LatestEventValue` has changed, it matches the previous (so the first) + // local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + + assert_eq!(buffer.buffer.len(), 1); + } + + // Receiving a `CancelledLocalEvent` targeting the first (so the last) event. + // The `LatestEventValue` cannot be computed from the send queue and will + // fallback to the event cache. The event cache is empty in this case, so we get + // nothing. + { + let update = + RoomSendQueueUpdate::CancelledLocalEvent { transaction_id: transaction_id_0 }; + + // The `LatestEventValue` has changed, it's empty! + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::None + ); + + assert!(buffer.buffer.is_empty()); + } + } + + #[async_test] + async fn test_local_sent_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + + // Receiving two `NewLocalEvent`s. + { + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { + let content = new_local_echo_content(&room_send_queue, transaction_id, body); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), body); + } + ); + } + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` + // must not change. + { + let update = RoomSendQueueUpdate::SentEvent { + transaction_id: transaction_id_0.clone(), + event_id: event_id!("$ev0").to_owned(), + }; + + // The `LatestEventValue` hasn't changed, it still matches the latest local + // event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 1); + } + + // Receiving a `SentEvent` targeting the first event. The `LaetstEvent` cannot + // be computed from the send queue and will fallback to the event cache. + // The event cache is empty in this case, so we get nothing. + { + let update = RoomSendQueueUpdate::SentEvent { + transaction_id: transaction_id_1, + event_id: event_id!("$ev1").to_owned(), + }; + + // The `LatestEventValue` has changed, it's empty! + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::None + ); + + assert!(buffer.buffer.is_empty()); + } + } + + #[async_test] + async fn test_local_replaced_local_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + + // Receiving two `NewLocalEvent`s. + { + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { + let content = new_local_echo_content(&room_send_queue, transaction_id, body); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), body); + } + ); + } + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `ReplacedLocalEvent` targeting the first event. The + // `LatestEventValue` must not change. + { + let transaction_id = &transaction_id_0; + let LocalEchoContent::Event { serialized_event: new_content, .. } = + new_local_echo_content(&room_send_queue, transaction_id, "A.") + else { + panic!("oopsy"); + }; + + let update = RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: transaction_id.clone(), + new_content, + }; + + // The `LatestEventValue` hasn't changed, it still matches the latest local + // event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `ReplacedLocalEvent` targeting the second (so the last) event. + // The `LatestEventValue` is changing. + { + let transaction_id = &transaction_id_1; + let LocalEchoContent::Event { serialized_event: new_content, .. } = + new_local_echo_content(&room_send_queue, transaction_id, "B.") + else { + panic!("oopsy"); + }; + + let update = RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: transaction_id.clone(), + new_content, + }; + + // The `LatestEventValue` has changed, it still matches the latest local + // event but with its new content. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B."); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + } + } + + #[async_test] + async fn test_local_send_error() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + + // Receiving two `NewLocalEvent`s. + { + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { + let content = new_local_echo_content(&room_send_queue, transaction_id, body); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), body); + } + ); + } + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `SendError` targeting the first event. The + // `LatestEventValue` must change to indicate it's wedged. + { + let update = RoomSendQueueUpdate::SendError { + transaction_id: transaction_id_0.clone(), + error: Arc::new(Error::UnknownError("oopsy".to_owned().into())), + is_recoverable: true, + }; + + // The `LatestEventValue` has changed, it still matches the latest local + // event but it's marked as wedged. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + assert_matches!( + &buffer.buffer[0].1, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + assert_matches!( + &buffer.buffer[1].1, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + + // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` + // must change: since an event has been sent, the following events are now + // unwedged. + { + let update = RoomSendQueueUpdate::SentEvent { + transaction_id: transaction_id_0.clone(), + event_id: event_id!("$ev0").to_owned(), + }; + + // The `LatestEventValue` has changed, it still matches the latest local + // event but it's unwedged. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 1); + assert_matches!( + &buffer.buffer[0].1, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + } + + #[async_test] + async fn test_local_retry_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id_0 = OwnedTransactionId::from("txnid0"); + let transaction_id_1 = OwnedTransactionId::from("txnid1"); + + // Receiving two `NewLocalEvent`s. + { + for (transaction_id, body) in [(&transaction_id_0, "A"), (&transaction_id_1, "B")] { + let content = new_local_echo_content(&room_send_queue, transaction_id, body); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), body); + } + ); + } + + assert_eq!(buffer.buffer.len(), 2); + } + + // Receiving a `SendError` targeting the first event. The + // `LatestEventValue` must change to indicate it's wedged. + { + let update = RoomSendQueueUpdate::SendError { + transaction_id: transaction_id_0.clone(), + error: Arc::new(Error::UnknownError("oopsy".to_owned().into())), + is_recoverable: true, + }; + + // The `LatestEventValue` has changed, it still matches the latest local + // event but it's marked as wedged. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + assert_matches!( + &buffer.buffer[0].1, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + assert_matches!( + &buffer.buffer[1].1, + LatestEventValue::LocalIsWedged( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + + // Receiving a `RetryEvent` targeting the first event. The `LatestEventValue` + // must change: this local event and its following must be unwedged. + { + let update = + RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id_0.clone() }; + + // The `LatestEventValue` has changed, it still matches the latest local + // event but it's unwedged. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + + assert_eq!(buffer.buffer.len(), 2); + assert_matches!( + &buffer.buffer[0].1, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + assert_matches!( + &buffer.buffer[1].1, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "B"); + } + ); + } + } + + #[async_test] + async fn test_local_media_upload() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id = OwnedTransactionId::from("txnid"); + + // Receiving a `NewLocalEvent`. + { + let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventKind::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + + assert_eq!(buffer.buffer.len(), 1); + } + + // Receiving a `MediaUpload` targeting the first event. The + // `LatestEventValue` must not change as `MediaUpload` are ignored. + { + let update = RoomSendQueueUpdate::MediaUpload { + related_to: transaction_id, + file: None, + index: 0, + progress: AbstractProgress { current: 0, total: 0 }, + }; + + // The `LatestEventValue` has changed somehow, it tells no new + // `LatestEventValue` is computed. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::None + ); + + assert_eq!(buffer.buffer.len(), 1); + } + } + + #[async_test] + async fn test_local_fallbacks_to_remote_when_empty() { + let room_id = room_id!("!r0"); + let user_id = user_id!("@mnt_io:matrix.org"); + let event_factory = EventFactory::new().sender(user_id).room(room_id); + let event_id_0 = event_id!("$ev0"); + let event_id_1 = event_id!("$ev1"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + + // Prelude. + { + // Create the room. + client.base_client().get_or_create_room(room_id, RoomState::Joined); + + // Initialise the event cache store. + client + .event_cache_store() + .lock() + .await + .unwrap() + .handle_linked_chunk_updates( + LinkedChunkId::Room(room_id), + vec![ + Update::NewItemsChunk { + previous: None, + new: ChunkIdentifier::new(0), + next: None, + }, + Update::PushItems { + at: Position::new(ChunkIdentifier::new(0), 0), + items: vec![event_factory + .text_msg("hello") + .event_id(event_id_0) + .into()], + }, + ], + ) + .await + .unwrap(); + } + + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + let mut buffer = LatestEventValuesForLocalEvents::new(); + + assert_matches!( + LatestEventValue::new_local( + // An update that won't be create a new `LatestEventValue`. + &RoomSendQueueUpdate::SentEvent { + transaction_id: OwnedTransactionId::from("txnid"), + event_id: event_id_1.to_owned(), + }, + &mut buffer, + &room_event_cache, + &None, + ) + .await, + // We get a `Remote` because there is no `Local*` values! + LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { + assert_eq!(message_content.body(), "hello"); } ); } diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 85a6998ba8d..9f014992573 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -1273,7 +1273,7 @@ mod tests { assert_matches!( latest_event_stream.get().await, LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { - assert_eq!(message_content.msgtype.body(), "world"); + assert_eq!(message_content.body(), "world"); } ); @@ -1296,7 +1296,7 @@ mod tests { assert_matches!( latest_event_stream.next().await, Some(LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content))) => { - assert_eq!(message_content.msgtype.body(), "raclette !"); + assert_eq!(message_content.body(), "raclette !"); } ); } diff --git a/crates/matrix-sdk/src/send_queue/mod.rs b/crates/matrix-sdk/src/send_queue/mod.rs index 98cc8937949..d7146031041 100644 --- a/crates/matrix-sdk/src/send_queue/mod.rs +++ b/crates/matrix-sdk/src/send_queue/mod.rs @@ -236,7 +236,7 @@ impl SendQueue { /// Get or create a new send queue for a given room, and insert it into our /// memoized rooms mapping. - fn for_room(&self, room: Room) -> RoomSendQueue { + pub(crate) fn for_room(&self, room: Room) -> RoomSendQueue { let data = self.data(); let mut map = data.rooms.write().unwrap(); @@ -2406,6 +2406,16 @@ pub struct SendHandle { } impl SendHandle { + /// Creates a new [`SendHandle`]. + #[cfg(test)] + pub(crate) fn new( + room: RoomSendQueue, + transaction_id: OwnedTransactionId, + created_at: MilliSecondsSinceUnixEpoch, + ) -> Self { + Self { room, transaction_id, media_handles: vec![], created_at } + } + fn nyi_for_uploads(&self) -> Result<(), RoomSendQueueStorageError> { if !self.media_handles.is_empty() { Err(RoomSendQueueStorageError::OperationNotImplementedYet) From 738dcb66f68dfeb6b03aacdf245dc8ba883e8d51 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 18 Aug 2025 12:19:46 +0200 Subject: [PATCH 08/17] test(sdk): Test that a `None` latest event value is ignored. --- .../src/latest_events/latest_event.rs | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 7fca5da8573..b161c1fd96c 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -1106,8 +1106,8 @@ mod tests_latest_event_value_non_wasm { }; use super::{ - LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, RoomEventCache, - RoomSendQueueUpdate, + LatestEvent, LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, + RoomEventCache, RoomSendQueueUpdate, }; use crate::{ client::WeakClient, @@ -1117,6 +1117,50 @@ mod tests_latest_event_value_non_wasm { Client, Error, }; + #[async_test] + async fn test_update_ignores_none_value() { + let room_id = room_id!("!r0"); + + let server = MatrixMockServer::new().await; + let client = server.client_builder().build().await; + let weak_client = WeakClient::from_client(&client); + + // Create the room. + client.base_client().get_or_create_room(room_id, RoomState::Joined); + let weak_room = WeakRoom::new(weak_client, room_id.to_owned()); + + // Get a `RoomEventCache`. + let event_cache = client.event_cache(); + event_cache.subscribe().unwrap(); + + let (room_event_cache, _) = event_cache.for_room(room_id).await.unwrap(); + + let mut latest_event = LatestEvent::new(room_id, None, &room_event_cache, &weak_room).await; + + // First off, check the default value is `None`! + assert_matches!(latest_event.current_value.get().await, LatestEventValue::None); + + // Second, set a new value. + latest_event + .update(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage( + RoomMessageEventContent::text_plain("foo"), + ))) + .await; + + assert_matches!( + latest_event.current_value.get().await, + LatestEventValue::LocalIsSending(_) + ); + + // Finally, set a new `None` value. It must be ignored. + latest_event.update(LatestEventValue::None).await; + + assert_matches!( + latest_event.current_value.get().await, + LatestEventValue::LocalIsSending(_) + ); + } + #[async_test] async fn test_remote_is_scanning_event_backwards_from_event_cache() { let room_id = room_id!("!r0"); From 470b0a9908cbdb6da1760174d6da49a94b1747eb Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Mon, 18 Aug 2025 15:02:05 +0200 Subject: [PATCH 09/17] feat(sdk): Introduce `LatestEventKind::Redacted`. This patch introduces `LatestEventKind::Redacted` to handle the case where an event is supposed to be a latest event but has been redacted. --- .../src/latest_events/latest_event.rs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index b161c1fd96c..68361b6dd72 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -27,7 +27,8 @@ use ruma::{ power_levels::RoomPowerLevels, }, sticker::StickerEventContent, - AnyMessageLikeEventContent, AnySyncStateEvent, AnySyncTimelineEvent, SyncStateEvent, + AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncStateEvent, + AnySyncTimelineEvent, SyncStateEvent, }, EventId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, TransactionId, UserId, }; @@ -517,7 +518,10 @@ pub enum LatestEventKind { /// A `m.room.member` event, more precisely a knock membership change that /// can be handled by the current user. - KnockedStateEvent(Option), + KnockedStateEvent(RoomMemberEventContent), + + /// A redacted event. + Redacted(AnySyncMessageLikeEvent), } fn find_and_map_timeline_event( @@ -540,13 +544,13 @@ fn find_and_map_timeline_event( } // The event has been redacted. - None => todo!("what to do with a redacted message-like event?"), + None => Some(LatestEventKind::Redacted(message_like_event)), } } // We don't currently support most state events… AnySyncTimelineEvent::State(state) => { - // … but we make an exception for knocked state events *if* the current user + // … but we make an exception for knocked state events _if_ the current user // can either accept or decline them. if let AnySyncStateEvent::RoomMember(member) = state { if matches!(member.membership(), MembershipState::Knock) { @@ -562,8 +566,12 @@ fn find_and_map_timeline_event( // displayed if can_accept_or_decline_knocks { return Some(LatestEventKind::KnockedStateEvent(match member { - SyncStateEvent::Original(member) => Some(member.content), - SyncStateEvent::Redacted(_) => None, + SyncStateEvent::Original(member) => member.content, + SyncStateEvent::Redacted(_) => { + // Cannot decide if the user can accept or decline knocks because + // the event has been redacted. + return None; + } })); } } @@ -650,8 +658,7 @@ mod tests_latest_event_kind { } #[test] - #[ignore] - fn test_room_message_redacted() { + fn test_redacted() { assert_latest_event_kind!( with |event_factory| { event_factory @@ -661,7 +668,7 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::RoomMessage(_)) + it produces Some(LatestEventKind::Redacted(_)) ); } From f4cac7d6114aa674f4fc05a4494ec2b90578a993 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 14:53:47 +0200 Subject: [PATCH 10/17] chore(sdk): Change `error!` to `warn!` when channels have been closed. This patch reduces the level of logs when channels have been closed in `LatestEvents`' tasks from `error` to `warn`. Indeed, when the `Client` shutdowns, the channels will be closed, but it's not an error at all. --- crates/matrix-sdk/src/latest_events/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 9f014992573..2bc3400c09c 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -66,7 +66,7 @@ use tokio::{ select, sync::{broadcast, mpsc, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; -use tracing::error; +use tracing::{error, warn}; use crate::{ client::WeakClient, @@ -608,7 +608,7 @@ async fn listen_to_event_cache_and_send_queue_updates_task( .await .is_break() { - error!("`listen_to_event_cache_and_send_queue_updates_task` has stopped"); + warn!("`listen_to_event_cache_and_send_queue_updates_task` has stopped"); break; } @@ -657,7 +657,7 @@ async fn listen_to_event_cache_and_send_queue_updates( }); } } else { - error!("`event_cache_generic_updates` channel has been closed"); + warn!("`event_cache_generic_updates` channel has been closed"); return ControlFlow::Break(()); } @@ -672,7 +672,7 @@ async fn listen_to_event_cache_and_send_queue_updates( }); } } else { - error!("`send_queue_generic_updates` channel has been closed"); + warn!("`send_queue_generic_updates` channel has been closed"); return ControlFlow::Break(()); } @@ -700,7 +700,7 @@ async fn compute_latest_events_task( buffer.clear(); } - error!("`compute_latest_events_task` has stopped"); + warn!("`compute_latest_events_task` has stopped"); } async fn compute_latest_events( From 3b1737f0e7b2f7b24c61db2dffc7d22b1e7054ca Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:12:54 +0200 Subject: [PATCH 11/17] refactor(sdk): Rename `LatestEventKind` to `LatestEventContent`. --- .../src/latest_events/latest_event.rs | 181 +++++++++--------- crates/matrix-sdk/src/latest_events/mod.rs | 8 +- 2 files changed, 97 insertions(+), 92 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 68361b6dd72..e8c9bf921f5 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -129,14 +129,14 @@ pub enum LatestEventValue { None, /// The latest event represents a remote event. - Remote(LatestEventKind), + Remote(LatestEventContent), /// The latest event represents a local event that is sending. - LocalIsSending(LatestEventKind), + LocalIsSending(LatestEventContent), /// The latest event represents a local event that is wedged, either because /// a previous local event, or this local event cannot be sent. - LocalIsWedged(LatestEventKind), + LocalIsWedged(LatestEventContent), } impl LatestEventValue { @@ -192,7 +192,7 @@ impl LatestEventValue { LocalEchoContent::Event { serialized_event: content, .. } => { if let Ok(content) = content.deserialize() { if let Some(kind) = find_and_map_any_message_like_event_content(content) { - let value = Self::LocalIsSending(kind); + let value = Self::LocalIsSending(content); buffer_of_values_for_local_events .push(transaction_id.to_owned(), value.clone()); @@ -256,7 +256,7 @@ impl LatestEventValue { if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { if let Ok(content) = content.deserialize() { if let Some(kind) = find_and_map_any_message_like_event_content(content) { - buffer_of_values_for_local_events.replace_kind(position, kind); + buffer_of_values_for_local_events.replace_content(position, content); } } else { return Self::None; @@ -407,8 +407,8 @@ impl LatestEventValuesForLocalEvents { self.buffer.push((transaction_id, value)); } - /// Replace the [`LatestEventKind`] of the [`LatestEventValue`] at position - /// `position`. + /// Replace the [`LatestEventContent`] of the [`LatestEventValue`] at + /// position `position`. /// /// # Panics /// @@ -417,12 +417,12 @@ impl LatestEventValuesForLocalEvents { /// - the [`LatestEventValue`] is not of kind /// [`LatestEventValue::LocalIsSending`] or /// [`LatestEventValue::LocalIsWedged`]. - fn replace_kind(&mut self, position: usize, new_kind: LatestEventKind) { + fn replace_content(&mut self, position: usize, new_content: LatestEventContent) { let (_, value) = self.buffer.get_mut(position).expect("`position` must be valid"); match value { - LatestEventValue::LocalIsSending(kind) => *kind = new_kind, - LatestEventValue::LocalIsWedged(kind) => *kind = new_kind, + LatestEventValue::LocalIsSending(content) => *content = new_content, + LatestEventValue::LocalIsWedged(content) => *content = new_content, _ => panic!("`value` must be either `LocalIsSending` or `LocalIsWedged`"), } } @@ -447,8 +447,8 @@ impl LatestEventValuesForLocalEvents { { // Iterate over the found value and the following ones. for (_, value_to_wedge) in once(first_value_to_wedge).chain(values) { - if let LatestEventValue::LocalIsSending(kind) = value_to_wedge { - *value_to_wedge = LatestEventValue::LocalIsWedged(kind.clone()); + if let LatestEventValue::LocalIsSending(content) = value_to_wedge { + *value_to_wedge = LatestEventValue::LocalIsWedged(content.clone()); } } } @@ -465,8 +465,8 @@ impl LatestEventValuesForLocalEvents { { // Iterate over the found value and the following ones. for (_, value_to_unwedge) in once(first_value_to_unwedge).chain(values) { - if let LatestEventValue::LocalIsWedged(kind) = value_to_unwedge { - *value_to_unwedge = LatestEventValue::LocalIsSending(kind.clone()); + if let LatestEventValue::LocalIsWedged(content) = value_to_unwedge { + *value_to_unwedge = LatestEventValue::LocalIsSending(content.clone()); } } } @@ -486,8 +486,8 @@ impl LatestEventValuesForLocalEvents { { // Iterate over all values after the found one. for (_, value_to_unwedge) in values { - if let LatestEventValue::LocalIsWedged(kind) = value_to_unwedge { - *value_to_unwedge = LatestEventValue::LocalIsSending(kind.clone()); + if let LatestEventValue::LocalIsWedged(content) = value_to_unwedge { + *value_to_unwedge = LatestEventValue::LocalIsSending(content.clone()); } } @@ -498,9 +498,9 @@ impl LatestEventValuesForLocalEvents { } } -/// A latest event value! +/// A latest event value content! #[derive(Debug, Clone)] -pub enum LatestEventKind { +pub enum LatestEventContent { /// A `m.room.message` event. RoomMessage(RoomMessageEventContent), @@ -527,7 +527,7 @@ pub enum LatestEventKind { fn find_and_map_timeline_event( event: &Event, power_levels: &Option<(&UserId, RoomPowerLevels)>, -) -> Option { +) -> Option { // Cast the event into an `AnySyncTimelineEvent`. If deserializing fails, we // ignore the event. let Some(event) = event.raw().deserialize().ok() else { @@ -544,7 +544,7 @@ fn find_and_map_timeline_event( } // The event has been redacted. - None => Some(LatestEventKind::Redacted(message_like_event)), + None => Some(LatestEventContent::Redacted(message_like_event)), } } @@ -565,7 +565,7 @@ fn find_and_map_timeline_event( // The current user can act on the knock changes, so they should be // displayed if can_accept_or_decline_knocks { - return Some(LatestEventKind::KnockedStateEvent(match member { + return Some(LatestEventContent::KnockedStateEvent(match member { SyncStateEvent::Original(member) => member.content, SyncStateEvent::Redacted(_) => { // Cannot decide if the user can accept or decline knocks because @@ -584,7 +584,7 @@ fn find_and_map_timeline_event( fn find_and_map_any_message_like_event_content( event: AnyMessageLikeEventContent, -) -> Option { +) -> Option { match event { AnyMessageLikeEventContent::RoomMessage(message) => { // Don't show incoming verification requests. @@ -605,17 +605,21 @@ fn find_and_map_any_message_like_event_content( if is_replacement { None } else { - Some(LatestEventKind::RoomMessage(message)) + Some(LatestEventContent::RoomMessage(message)) } } - AnyMessageLikeEventContent::UnstablePollStart(poll) => Some(LatestEventKind::Poll(poll)), + AnyMessageLikeEventContent::UnstablePollStart(poll) => Some(LatestEventContent::Poll(poll)), - AnyMessageLikeEventContent::CallInvite(invite) => Some(LatestEventKind::CallInvite(invite)), + AnyMessageLikeEventContent::CallInvite(invite) => { + Some(LatestEventContent::CallInvite(invite)) + } - AnyMessageLikeEventContent::CallNotify(notify) => Some(LatestEventKind::CallNotify(notify)), + AnyMessageLikeEventContent::CallNotify(notify) => { + Some(LatestEventContent::CallNotify(notify)) + } - AnyMessageLikeEventContent::Sticker(sticker) => Some(LatestEventKind::Sticker(sticker)), + AnyMessageLikeEventContent::Sticker(sticker) => Some(LatestEventContent::Sticker(sticker)), // Encrypted events are not suitable. AnyMessageLikeEventContent::RoomEncrypted(_) => None, @@ -626,14 +630,14 @@ fn find_and_map_any_message_like_event_content( } #[cfg(test)] -mod tests_latest_event_kind { +mod tests_latest_event_content { use assert_matches::assert_matches; use matrix_sdk_test::event_factory::EventFactory; use ruma::{event_id, user_id}; - use super::{find_and_map_timeline_event, LatestEventKind, RoomMessageEventContent}; + use super::{find_and_map_timeline_event, LatestEventContent, RoomMessageEventContent}; - macro_rules! assert_latest_event_kind { + macro_rules! assert_latest_event_content { ( with | $event_factory:ident | $event_builder:block it produces $match:pat ) => { let user_id = user_id!("@mnt_io:matrix.org"); @@ -649,17 +653,17 @@ mod tests_latest_event_kind { #[test] fn test_room_message() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory.text_msg("hello").into_event() } - it produces Some(LatestEventKind::RoomMessage(_)) + it produces Some(LatestEventContent::RoomMessage(_)) ); } #[test] fn test_redacted() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .redacted( @@ -668,13 +672,13 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::Redacted(_)) + it produces Some(LatestEventContent::Redacted(_)) ); } #[test] fn test_room_message_replacement() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .text_msg("bonjour") @@ -690,7 +694,7 @@ mod tests_latest_event_kind { #[test] fn test_poll() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .poll_start( @@ -700,13 +704,13 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::Poll(_)) + it produces Some(LatestEventContent::Poll(_)) ); } #[test] fn test_call_invite() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .call_invite( @@ -717,13 +721,13 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::CallInvite(_)) + it produces Some(LatestEventContent::CallInvite(_)) ); } #[test] fn test_call_notify() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .call_notify( @@ -734,13 +738,13 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::CallNotify(_)) + it produces Some(LatestEventContent::CallNotify(_)) ); } #[test] fn test_sticker() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .sticker( @@ -750,13 +754,13 @@ mod tests_latest_event_kind { ) .into_event() } - it produces Some(LatestEventKind::Sticker(_)) + it produces Some(LatestEventContent::Sticker(_)) ); } #[test] fn test_encrypted_room_message() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .event(ruma::events::room::encrypted::RoomEncryptedEventContent::new( @@ -780,7 +784,7 @@ mod tests_latest_event_kind { #[test] fn test_reaction() { // Take a random message-like event. - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .reaction(event_id!("$ev0"), "+1") @@ -792,7 +796,7 @@ mod tests_latest_event_kind { #[test] fn test_state_event() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .room_topic("new room topic") @@ -804,7 +808,7 @@ mod tests_latest_event_kind { #[test] fn test_knocked_state_event_without_power_levels() { - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .member(user_id!("@other_mnt_io:server.name")) @@ -854,7 +858,7 @@ mod tests_latest_event_kind { room_power_levels.kick = 10.into(); assert_matches!( find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), - Some(LatestEventKind::KnockedStateEvent(_)), + Some(LatestEventContent::KnockedStateEvent(_)), "can accept, cannot decline", ); } @@ -866,7 +870,7 @@ mod tests_latest_event_kind { room_power_levels.kick = 0.into(); assert_matches!( find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), - Some(LatestEventKind::KnockedStateEvent(_)), + Some(LatestEventContent::KnockedStateEvent(_)), "cannot accept, can decline", ); } @@ -877,7 +881,7 @@ mod tests_latest_event_kind { room_power_levels.kick = 0.into(); assert_matches!( find_and_map_timeline_event(&event, &Some((user_id, room_power_levels))), - Some(LatestEventKind::KnockedStateEvent(_)), + Some(LatestEventContent::KnockedStateEvent(_)), "can accept, can decline", ); } @@ -887,7 +891,7 @@ mod tests_latest_event_kind { fn test_room_message_verification_request() { use ruma::{events::room::message, OwnedDeviceId}; - assert_latest_event_kind!( + assert_latest_event_content!( with |event_factory| { event_factory .event( @@ -915,11 +919,12 @@ mod tests_latest_event_values_for_local_events { use ruma::OwnedTransactionId; use super::{ - LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, RoomMessageEventContent, + LatestEventContent, LatestEventValue, LatestEventValuesForLocalEvents, + RoomMessageEventContent, }; - fn room_message(body: &str) -> LatestEventKind { - LatestEventKind::RoomMessage(RoomMessageEventContent::text_plain(body)) + fn room_message(body: &str) -> LatestEventContent { + LatestEventContent::RoomMessage(RoomMessageEventContent::text_plain(body)) } #[test] @@ -935,7 +940,7 @@ mod tests_latest_event_values_for_local_events { assert_matches!( buffer.last(), - Some(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(_))) + Some(LatestEventValue::LocalIsSending(LatestEventContent::RoomMessage(_))) ); } @@ -994,7 +999,7 @@ mod tests_latest_event_values_for_local_events { } #[test] - fn test_replace_kind() { + fn test_replace_content() { let mut buffer = LatestEventValuesForLocalEvents::new(); buffer.push( @@ -1002,11 +1007,11 @@ mod tests_latest_event_values_for_local_events { LatestEventValue::LocalIsSending(room_message("gruyère")), ); - buffer.replace_kind(0, room_message("comté")); + buffer.replace_content(0, room_message("comté")); assert_matches!( buffer.last(), - Some(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(content))) => { + Some(LatestEventValue::LocalIsSending(LatestEventContent::RoomMessage(content))) => { assert_eq!(content.body(), "comté"); } ); @@ -1113,7 +1118,7 @@ mod tests_latest_event_value_non_wasm { }; use super::{ - LatestEvent, LatestEventKind, LatestEventValue, LatestEventValuesForLocalEvents, + LatestEvent, LatestEventContent, LatestEventValue, LatestEventValuesForLocalEvents, RoomEventCache, RoomSendQueueUpdate, }; use crate::{ @@ -1149,7 +1154,7 @@ mod tests_latest_event_value_non_wasm { // Second, set a new value. latest_event - .update(LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage( + .update(LatestEventValue::LocalIsSending(LatestEventContent::RoomMessage( RoomMessageEventContent::text_plain("foo"), ))) .await; @@ -1227,7 +1232,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_remote(&room_event_cache, &weak_room).await, - LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { + LatestEventValue::Remote(LatestEventContent::RoomMessage(message_content)) => { // We get `event_id_1` because `event_id_2` isn't a candidate, // and `event_id_0` hasn't been read yet (because events are // read backwards). @@ -1290,7 +1295,7 @@ mod tests_latest_event_value_non_wasm { // The `LatestEventValue` matches the new local event. assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, - LatestEventValue::LocalIsSending(LatestEventKind::RoomMessage(message_content)) => { + LatestEventValue::LocalIsSending(LatestEventContent::RoomMessage(message_content)) => { assert_eq!(message_content.body(), "A"); } ); @@ -1307,7 +1312,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1318,7 +1323,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[0].1, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1326,7 +1331,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[1].1, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1358,7 +1363,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), body); } @@ -1380,7 +1385,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "C"); } @@ -1401,7 +1406,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1450,7 +1455,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), body); } @@ -1473,7 +1478,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1523,7 +1528,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), body); } @@ -1553,7 +1558,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1582,7 +1587,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B."); } @@ -1614,7 +1619,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), body); } @@ -1638,7 +1643,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1648,7 +1653,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[0].1, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1656,7 +1661,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[1].1, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1677,7 +1682,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1687,7 +1692,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[0].1, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1717,7 +1722,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), body); } @@ -1741,7 +1746,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1751,7 +1756,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[0].1, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1759,7 +1764,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[1].1, LatestEventValue::LocalIsWedged( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1777,7 +1782,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1787,7 +1792,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[0].1, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1795,7 +1800,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( &buffer.buffer[1].1, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); } @@ -1823,7 +1828,7 @@ mod tests_latest_event_value_non_wasm { assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( - LatestEventKind::RoomMessage(message_content) + LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); } @@ -1916,7 +1921,7 @@ mod tests_latest_event_value_non_wasm { ) .await, // We get a `Remote` because there is no `Local*` values! - LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { + LatestEventValue::Remote(LatestEventContent::RoomMessage(message_content)) => { assert_eq!(message_content.body(), "hello"); } ); diff --git a/crates/matrix-sdk/src/latest_events/mod.rs b/crates/matrix-sdk/src/latest_events/mod.rs index 2bc3400c09c..5ee34946f2e 100644 --- a/crates/matrix-sdk/src/latest_events/mod.rs +++ b/crates/matrix-sdk/src/latest_events/mod.rs @@ -59,7 +59,7 @@ pub use error::LatestEventsError; use eyeball::{AsyncLock, Subscriber}; use futures_util::FutureExt; use latest_event::LatestEvent; -pub use latest_event::{LatestEventKind, LatestEventValue}; +pub use latest_event::{LatestEventContent, LatestEventValue}; use matrix_sdk_common::executor::{spawn, AbortOnDrop, JoinHandleExt as _}; use ruma::{EventId, OwnedEventId, OwnedRoomId, RoomId}; use tokio::{ @@ -750,7 +750,7 @@ mod tests { use stream_assert::assert_pending; use super::{ - broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventKind, + broadcast, listen_to_event_cache_and_send_queue_updates, mpsc, HashSet, LatestEventContent, LatestEventValue, RoomEventCacheGenericUpdate, RoomRegistration, RoomSendQueueUpdate, SendQueueUpdate, }; @@ -1272,7 +1272,7 @@ mod tests { // latest event! assert_matches!( latest_event_stream.get().await, - LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content)) => { + LatestEventValue::Remote(LatestEventContent::RoomMessage(message_content)) => { assert_eq!(message_content.body(), "world"); } ); @@ -1295,7 +1295,7 @@ mod tests { // `compute_latest_events` which has updated the latest event value. assert_matches!( latest_event_stream.next().await, - Some(LatestEventValue::Remote(LatestEventKind::RoomMessage(message_content))) => { + Some(LatestEventValue::Remote(LatestEventContent::RoomMessage(message_content))) => { assert_eq!(message_content.body(), "raclette !"); } ); From 81880a72857d3109515f1cfe3a0bc2c1e924932e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:13:31 +0200 Subject: [PATCH 12/17] =?UTF-8?q?refactor(sdk):=20Rename=20`find=5Fand=5Fm?= =?UTF-8?q?ap=5Fany=5Fmessage=E2=80=A6`=20to=20`extract=5Fcontent=5Ffrom?= =?UTF-8?q?=5Fany=5Fmessage=5Flike`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/matrix-sdk/src/latest_events/latest_event.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index e8c9bf921f5..b58fb73237a 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -191,7 +191,7 @@ impl LatestEventValue { }) => match local_echo_content { LocalEchoContent::Event { serialized_event: content, .. } => { if let Ok(content) = content.deserialize() { - if let Some(kind) = find_and_map_any_message_like_event_content(content) { + if let Some(content) = extract_content_from_any_message_like(content) { let value = Self::LocalIsSending(content); buffer_of_values_for_local_events @@ -255,7 +255,7 @@ impl LatestEventValue { RoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, new_content: content } => { if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { if let Ok(content) = content.deserialize() { - if let Some(kind) = find_and_map_any_message_like_event_content(content) { + if let Some(content) = extract_content_from_any_message_like(content) { buffer_of_values_for_local_events.replace_content(position, content); } } else { @@ -540,7 +540,7 @@ fn find_and_map_timeline_event( AnySyncTimelineEvent::MessageLike(message_like_event) => { match message_like_event.original_content() { Some(any_message_like_event_content) => { - find_and_map_any_message_like_event_content(any_message_like_event_content) + extract_content_from_any_message_like(any_message_like_event_content) } // The event has been redacted. @@ -582,7 +582,7 @@ fn find_and_map_timeline_event( } } -fn find_and_map_any_message_like_event_content( +fn extract_content_from_any_message_like( event: AnyMessageLikeEventContent, ) -> Option { match event { From a70dc59ef2ec6817681d8c4479ee11181c2ca34e Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:23:25 +0200 Subject: [PATCH 13/17] chore(sdk): Log all `deserialize` errors in `latest_events`. --- .../src/latest_events/latest_event.rs | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index b58fb73237a..80033e26ba2 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -32,7 +32,7 @@ use ruma::{ }, EventId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId, TransactionId, UserId, }; -use tracing::warn; +use tracing::error; use crate::{event_cache::RoomEventCache, room::WeakRoom, send_queue::RoomSendQueueUpdate}; @@ -190,19 +190,25 @@ impl LatestEventValue { content: local_echo_content, }) => match local_echo_content { LocalEchoContent::Event { serialized_event: content, .. } => { - if let Ok(content) = content.deserialize() { - if let Some(content) = extract_content_from_any_message_like(content) { - let value = Self::LocalIsSending(content); + match content.deserialize() { + Ok(content) => { + if let Some(content) = extract_content_from_any_message_like(content) { + let value = Self::LocalIsSending(content); - buffer_of_values_for_local_events - .push(transaction_id.to_owned(), value.clone()); + buffer_of_values_for_local_events + .push(transaction_id.to_owned(), value.clone()); + + value + } else { + Self::None + } + } + + Err(error) => { + error!(?error, "Failed to deserialize an event from `RoomSendQueueUpdate::NewLocalEvent`"); - value - } else { Self::None } - } else { - Self::None } } @@ -254,12 +260,19 @@ impl LatestEventValue { // (note: it should!), and return the last `LatestEventValue` or calculate a new one. RoomSendQueueUpdate::ReplacedLocalEvent { transaction_id, new_content: content } => { if let Some(position) = buffer_of_values_for_local_events.position(transaction_id) { - if let Ok(content) = content.deserialize() { - if let Some(content) = extract_content_from_any_message_like(content) { - buffer_of_values_for_local_events.replace_content(position, content); + match content.deserialize() { + Ok(content) => { + if let Some(content) = extract_content_from_any_message_like(content) { + buffer_of_values_for_local_events + .replace_content(position, content); + } + } + + Err(error) => { + error!(?error, "Failed to deserialize an event from `RoomSendQueueUpdate::ReplacedLocalEvent`"); + + return Self::None; } - } else { - return Self::None; } } @@ -530,10 +543,16 @@ fn find_and_map_timeline_event( ) -> Option { // Cast the event into an `AnySyncTimelineEvent`. If deserializing fails, we // ignore the event. - let Some(event) = event.raw().deserialize().ok() else { - warn!(?event, "Failed to deserialize the event when looking for a suitable latest event"); + let event = match event.raw().deserialize() { + Ok(event) => event, + Err(error) => { + error!( + ?error, + "Failed to deserialize the event when looking for a suitable latest event" + ); - return None; + return None; + } }; match event { From 3487a25f4ca1f0aa68855174027e9cf87a6c82b5 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:39:05 +0200 Subject: [PATCH 14/17] feat(sdk): Handle replacing a local event by a non-suitable latest event value. This patch handles the case where a local event is replaced by another local event which isn't suitable for being a latest event value. In this case, the previous existing latest event value should be removed from the buffer. --- .../src/latest_events/latest_event.rs | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 80033e26ba2..c449f03b766 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -265,6 +265,8 @@ impl LatestEventValue { if let Some(content) = extract_content_from_any_message_like(content) { buffer_of_values_for_local_events .replace_content(position, content); + } else { + buffer_of_values_for_local_events.remove(position); } } @@ -1132,7 +1134,10 @@ mod tests_latest_event_value_non_wasm { use matrix_sdk_test::{async_test, event_factory::EventFactory}; use ruma::{ event_id, - events::{room::message::RoomMessageEventContent, AnyMessageLikeEventContent}, + events::{ + reaction::ReactionEventContent, relation::Annotation, + room::message::RoomMessageEventContent, AnyMessageLikeEventContent, + }, room_id, user_id, MilliSecondsSinceUnixEpoch, OwnedRoomId, OwnedTransactionId, }; @@ -1616,6 +1621,63 @@ mod tests_latest_event_value_non_wasm { } } + #[async_test] + async fn test_local_replaced_local_event_by_a_non_suitable_event() { + let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; + + let mut buffer = LatestEventValuesForLocalEvents::new(); + let transaction_id = OwnedTransactionId::from("txnid0"); + + // Receiving one `NewLocalEvent`. + { + let content = new_local_echo_content(&room_send_queue, &transaction_id, "A"); + + let update = RoomSendQueueUpdate::NewLocalEvent(LocalEcho { + transaction_id: transaction_id.clone(), + content, + }); + + // The `LatestEventValue` matches the new local event. + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::LocalIsSending( + LatestEventContent::RoomMessage(message_content) + ) => { + assert_eq!(message_content.body(), "A"); + } + ); + + assert_eq!(buffer.buffer.len(), 1); + } + + // Receiving a `ReplacedLocalEvent` targeting the first event. Sadly, the new + // event cannot be mapped to a `LatestEventValue`! The first event is removed + // from the buffer, and the `LatestEventValue` becomes `None` because there is + // no other alternative. + { + let new_content = SerializableEventContent::new(&AnyMessageLikeEventContent::Reaction( + ReactionEventContent::new(Annotation::new( + event_id!("$ev0").to_owned(), + "+1".to_owned(), + )), + )) + .unwrap(); + + let update = RoomSendQueueUpdate::ReplacedLocalEvent { + transaction_id: transaction_id.clone(), + new_content, + }; + + // The `LatestEventValue` has changed! + assert_matches!( + LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, + LatestEventValue::None + ); + + assert_eq!(buffer.buffer.len(), 0); + } + } + #[async_test] async fn test_local_send_error() { let (_client, _room_id, room_send_queue, room_event_cache) = local_prelude().await; From ff0990e6045087f4d7448918f4033de69723b48d Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:44:33 +0200 Subject: [PATCH 15/17] chore(sdk): Add `mark_` prefix. --- .../src/latest_events/latest_event.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index c449f03b766..50417f47e50 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -240,7 +240,8 @@ impl LatestEventValue { // `LatestEventValue` from the buffer of values. Finally, return the last // `LatestEventValue` or calculate a new one. RoomSendQueueUpdate::SentEvent { transaction_id, .. } => { - let position = buffer_of_values_for_local_events.unwedged_after(transaction_id); + let position = + buffer_of_values_for_local_events.mark_unwedged_after(transaction_id); if let Some(position) = position { buffer_of_values_for_local_events.remove(position); @@ -291,7 +292,7 @@ impl LatestEventValue { // Mark the latest event value matching `transaction_id`, and all its following values, // as wedged. RoomSendQueueUpdate::SendError { transaction_id, .. } => { - buffer_of_values_for_local_events.wedged_from(transaction_id); + buffer_of_values_for_local_events.mark_wedged_from(transaction_id); Self::new_local_or_remote( buffer_of_values_for_local_events, @@ -306,7 +307,7 @@ impl LatestEventValue { // Mark the latest event value matching `transaction_id`, and all its following values, // as unwedged. RoomSendQueueUpdate::RetryEvent { transaction_id } => { - buffer_of_values_for_local_events.unwedged_from(transaction_id); + buffer_of_values_for_local_events.mark_unwedged_from(transaction_id); Self::new_local_or_remote( buffer_of_values_for_local_events, @@ -453,7 +454,7 @@ impl LatestEventValuesForLocalEvents { /// Mark the `LatestEventValue` matching `transaction_id`, and all the /// following values, as wedged. - fn wedged_from(&mut self, transaction_id: &TransactionId) { + fn mark_wedged_from(&mut self, transaction_id: &TransactionId) { let mut values = self.buffer.iter_mut(); if let Some(first_value_to_wedge) = values @@ -471,7 +472,7 @@ impl LatestEventValuesForLocalEvents { /// Mark the `LatestEventValue` matching `transaction_id`, and all the /// following values, as unwedged. - fn unwedged_from(&mut self, transaction_id: &TransactionId) { + fn mark_unwedged_from(&mut self, transaction_id: &TransactionId) { let mut values = self.buffer.iter_mut(); if let Some(first_value_to_unwedge) = values @@ -492,7 +493,7 @@ impl LatestEventValuesForLocalEvents { /// /// Note that contrary to [`Self::unwedged_from`], the `LatestEventValue` is /// untouched. However, its position is returned (if any). - fn unwedged_after(&mut self, transaction_id: &TransactionId) -> Option { + fn mark_unwedged_after(&mut self, transaction_id: &TransactionId) -> Option { let mut values = self.buffer.iter_mut(); if let Some(position) = values @@ -1068,7 +1069,7 @@ mod tests_latest_event_values_for_local_events { ); buffer.push(transaction_id_2, LatestEventValue::LocalIsSending(room_message("raclette"))); - buffer.wedged_from(&transaction_id_1); + buffer.mark_wedged_from(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsSending(_)); @@ -1090,7 +1091,7 @@ mod tests_latest_event_values_for_local_events { ); buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); - buffer.unwedged_from(&transaction_id_1); + buffer.mark_unwedged_from(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); @@ -1112,7 +1113,7 @@ mod tests_latest_event_values_for_local_events { ); buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); - buffer.unwedged_after(&transaction_id_1); + buffer.mark_unwedged_after(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); From 94bf104e8155d899bfce6b4d4ca16355030182ce Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 15:50:50 +0200 Subject: [PATCH 16/17] doc(sdk): Add a `TODO` marker for later. --- crates/matrix-sdk/src/latest_events/latest_event.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 50417f47e50..41c42fd4af0 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -616,6 +616,9 @@ fn extract_content_from_any_message_like( // Check if this is a replacement for another message. If it is, ignore // it. + // + // TODO: if we want to support something like + // `LatestEventContent::EditedRoomMessage`, it's here :-]. let is_replacement = message.relates_to.as_ref().is_some_and(|relates_to| { if let Some(relation_type) = relates_to.rel_type() { relation_type == RelationType::Replacement From 96b3a6af242f0a208d1f179be9ab877b0ab16654 Mon Sep 17 00:00:00 2001 From: Ivan Enderlin Date: Tue, 19 Aug 2025 16:19:25 +0200 Subject: [PATCH 17/17] refactor(sdk): Rename `LocalIsWedged` to `LocalCannotBeSent`. This patch renames `LocalIsWedged` to `LocalCannotBeSent` to avoid confusion with the wedged/unwedged state of the `send_queue`. The semantics is different in `latest_events`. --- .../src/latest_events/latest_event.rs | 136 +++++++++--------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/crates/matrix-sdk/src/latest_events/latest_event.rs b/crates/matrix-sdk/src/latest_events/latest_event.rs index 41c42fd4af0..fc13b338a57 100644 --- a/crates/matrix-sdk/src/latest_events/latest_event.rs +++ b/crates/matrix-sdk/src/latest_events/latest_event.rs @@ -134,9 +134,9 @@ pub enum LatestEventValue { /// The latest event represents a local event that is sending. LocalIsSending(LatestEventContent), - /// The latest event represents a local event that is wedged, either because - /// a previous local event, or this local event cannot be sent. - LocalIsWedged(LatestEventContent), + /// The latest event represents a local event that cannot be sent, either + /// because a previous local event, or this local event cannot be sent. + LocalCannotBeSent(LatestEventContent), } impl LatestEventValue { @@ -172,7 +172,7 @@ impl LatestEventValue { } /// Create a new [`LatestEventValue::LocalIsSending`] or - /// [`LatestEventValue::LocalIsWedged`]. + /// [`LatestEventValue::LocalCannotBeSent`]. async fn new_local( send_queue_update: &RoomSendQueueUpdate, buffer_of_values_for_local_events: &mut LatestEventValuesForLocalEvents, @@ -234,14 +234,15 @@ impl LatestEventValue { // A local event has successfully been sent! // - // Unwedge all wedged values after the one matching `transaction_id`. Indeed, if - // an event has been sent, it means the send queue is working, so if any value has been - // marked as wedged, it must be marked as unwedged. Then, remove the calculated - // `LatestEventValue` from the buffer of values. Finally, return the last - // `LatestEventValue` or calculate a new one. + // Mark all “cannot be sent” values as “is sending” after the one matching + // `transaction_id`. Indeed, if an event has been sent, it means the send queue is + // working, so if any value has been marked as “cannot be sent”, it must be marked as + // “is sending”. Then, remove the calculated `LatestEventValue` from the buffer of + // values. Finally, return the last `LatestEventValue` or calculate a new + // one. RoomSendQueueUpdate::SentEvent { transaction_id, .. } => { let position = - buffer_of_values_for_local_events.mark_unwedged_after(transaction_id); + buffer_of_values_for_local_events.mark_is_sending_after(transaction_id); if let Some(position) = position { buffer_of_values_for_local_events.remove(position); @@ -290,9 +291,9 @@ impl LatestEventValue { // An error has occurred. // // Mark the latest event value matching `transaction_id`, and all its following values, - // as wedged. + // as “cannot be sent”. RoomSendQueueUpdate::SendError { transaction_id, .. } => { - buffer_of_values_for_local_events.mark_wedged_from(transaction_id); + buffer_of_values_for_local_events.mark_cannot_be_sent_from(transaction_id); Self::new_local_or_remote( buffer_of_values_for_local_events, @@ -305,9 +306,9 @@ impl LatestEventValue { // A local event has been unwedged and sending is being retried. // // Mark the latest event value matching `transaction_id`, and all its following values, - // as unwedged. + // as “is sending”. RoomSendQueueUpdate::RetryEvent { transaction_id } => { - buffer_of_values_for_local_events.mark_unwedged_from(transaction_id); + buffer_of_values_for_local_events.mark_is_sending_from(transaction_id); Self::new_local_or_remote( buffer_of_values_for_local_events, @@ -376,9 +377,9 @@ impl LatestEventValue { /// Because a `SendError` is received (targeting the first `NewLocalEvent`), the /// send queue is stopped. However, the `LatestEventValue` targets the second /// `NewLocalEvent`. The system must consider that when a local event is wedged, -/// all the following local events must also be marked as wedged. And vice -/// versa, when the send queue is able to send an event again, all the following -/// local events must be marked as unwedged. +/// all the following local events must also be marked as “cannot be sent”. And +/// vice versa, when the send queue is able to send an event again, all the +/// following local events must be marked as “is sending”. /// /// This type isolates a couple of methods designed to manage these specific /// behaviours. @@ -410,14 +411,14 @@ impl LatestEventValuesForLocalEvents { /// # Panics /// /// Panics if `value` is not of kind [`LatestEventValue::LocalIsSending`] or - /// [`LatestEventValue::LocalIsWedged`]. + /// [`LatestEventValue::LocalCannotBeSent`]. fn push(&mut self, transaction_id: OwnedTransactionId, value: LatestEventValue) { assert!( matches!( value, - LatestEventValue::LocalIsSending(_) | LatestEventValue::LocalIsWedged(_) + LatestEventValue::LocalIsSending(_) | LatestEventValue::LocalCannotBeSent(_) ), - "`value` must be either `LocalIsSending` or `LocalIsWedged`" + "`value` must be either `LocalIsSending` or `LocalCannotBeSent`" ); self.buffer.push((transaction_id, value)); @@ -432,14 +433,14 @@ impl LatestEventValuesForLocalEvents { /// - `position` is strictly greater than buffer's length, /// - the [`LatestEventValue`] is not of kind /// [`LatestEventValue::LocalIsSending`] or - /// [`LatestEventValue::LocalIsWedged`]. + /// [`LatestEventValue::LocalCannotBeSent`]. fn replace_content(&mut self, position: usize, new_content: LatestEventContent) { let (_, value) = self.buffer.get_mut(position).expect("`position` must be valid"); match value { LatestEventValue::LocalIsSending(content) => *content = new_content, - LatestEventValue::LocalIsWedged(content) => *content = new_content, - _ => panic!("`value` must be either `LocalIsSending` or `LocalIsWedged`"), + LatestEventValue::LocalCannotBeSent(content) => *content = new_content, + _ => panic!("`value` must be either `LocalIsSending` or `LocalCannotBeSent`"), } } @@ -453,8 +454,8 @@ impl LatestEventValuesForLocalEvents { } /// Mark the `LatestEventValue` matching `transaction_id`, and all the - /// following values, as wedged. - fn mark_wedged_from(&mut self, transaction_id: &TransactionId) { + /// following values, as “cannot be sent”. + fn mark_cannot_be_sent_from(&mut self, transaction_id: &TransactionId) { let mut values = self.buffer.iter_mut(); if let Some(first_value_to_wedge) = values @@ -464,15 +465,15 @@ impl LatestEventValuesForLocalEvents { // Iterate over the found value and the following ones. for (_, value_to_wedge) in once(first_value_to_wedge).chain(values) { if let LatestEventValue::LocalIsSending(content) = value_to_wedge { - *value_to_wedge = LatestEventValue::LocalIsWedged(content.clone()); + *value_to_wedge = LatestEventValue::LocalCannotBeSent(content.clone()); } } } } /// Mark the `LatestEventValue` matching `transaction_id`, and all the - /// following values, as unwedged. - fn mark_unwedged_from(&mut self, transaction_id: &TransactionId) { + /// following values, as “is sending”. + fn mark_is_sending_from(&mut self, transaction_id: &TransactionId) { let mut values = self.buffer.iter_mut(); if let Some(first_value_to_unwedge) = values @@ -481,7 +482,7 @@ impl LatestEventValuesForLocalEvents { { // Iterate over the found value and the following ones. for (_, value_to_unwedge) in once(first_value_to_unwedge).chain(values) { - if let LatestEventValue::LocalIsWedged(content) = value_to_unwedge { + if let LatestEventValue::LocalCannotBeSent(content) = value_to_unwedge { *value_to_unwedge = LatestEventValue::LocalIsSending(content.clone()); } } @@ -489,11 +490,12 @@ impl LatestEventValuesForLocalEvents { } /// Mark all the following values after the `LatestEventValue` matching - /// `transaction_id` as unwedged. + /// `transaction_id` as “is sending”. /// - /// Note that contrary to [`Self::unwedged_from`], the `LatestEventValue` is - /// untouched. However, its position is returned (if any). - fn mark_unwedged_after(&mut self, transaction_id: &TransactionId) -> Option { + /// Note that contrary to [`Self::mark_is_sending_from`], the + /// `LatestEventValue` is untouched. However, its position is returned + /// (if any). + fn mark_is_sending_after(&mut self, transaction_id: &TransactionId) -> Option { let mut values = self.buffer.iter_mut(); if let Some(position) = values @@ -502,7 +504,7 @@ impl LatestEventValuesForLocalEvents { { // Iterate over all values after the found one. for (_, value_to_unwedge) in values { - if let LatestEventValue::LocalIsWedged(content) = value_to_unwedge { + if let LatestEventValue::LocalCannotBeSent(content) = value_to_unwedge { *value_to_unwedge = LatestEventValue::LocalIsSending(content.clone()); } } @@ -1017,7 +1019,7 @@ mod tests_latest_event_values_for_local_events { ); buffer.push( OwnedTransactionId::from("txnid1"), - LatestEventValue::LocalIsWedged(room_message("raclette")), + LatestEventValue::LocalCannotBeSent(room_message("raclette")), ); // no panic. @@ -1059,7 +1061,7 @@ mod tests_latest_event_values_for_local_events { } #[test] - fn test_wedged_from() { + fn test_mark_cannot_be_sent_from() { let mut buffer = LatestEventValuesForLocalEvents::new(); let transaction_id_0 = OwnedTransactionId::from("txnid0"); let transaction_id_1 = OwnedTransactionId::from("txnid1"); @@ -1072,55 +1074,57 @@ mod tests_latest_event_values_for_local_events { ); buffer.push(transaction_id_2, LatestEventValue::LocalIsSending(room_message("raclette"))); - buffer.mark_wedged_from(&transaction_id_1); + buffer.mark_cannot_be_sent_from(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsSending(_)); - assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsWedged(_)); - assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalCannotBeSent(_)); + assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalCannotBeSent(_)); } #[test] - fn test_unwedged_from() { + fn test_mark_is_sending_from() { let mut buffer = LatestEventValuesForLocalEvents::new(); let transaction_id_0 = OwnedTransactionId::from("txnid0"); let transaction_id_1 = OwnedTransactionId::from("txnid1"); let transaction_id_2 = OwnedTransactionId::from("txnid2"); - buffer.push(transaction_id_0, LatestEventValue::LocalIsWedged(room_message("gruyère"))); + buffer.push(transaction_id_0, LatestEventValue::LocalCannotBeSent(room_message("gruyère"))); buffer.push( transaction_id_1.clone(), - LatestEventValue::LocalIsWedged(room_message("brigand")), + LatestEventValue::LocalCannotBeSent(room_message("brigand")), ); - buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); + buffer + .push(transaction_id_2, LatestEventValue::LocalCannotBeSent(room_message("raclette"))); - buffer.mark_unwedged_from(&transaction_id_1); + buffer.mark_is_sending_from(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); - assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalCannotBeSent(_)); assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsSending(_)); assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsSending(_)); } #[test] - fn test_unwedged_after() { + fn test_mark_is_sending_after() { let mut buffer = LatestEventValuesForLocalEvents::new(); let transaction_id_0 = OwnedTransactionId::from("txnid0"); let transaction_id_1 = OwnedTransactionId::from("txnid1"); let transaction_id_2 = OwnedTransactionId::from("txnid2"); - buffer.push(transaction_id_0, LatestEventValue::LocalIsWedged(room_message("gruyère"))); + buffer.push(transaction_id_0, LatestEventValue::LocalCannotBeSent(room_message("gruyère"))); buffer.push( transaction_id_1.clone(), - LatestEventValue::LocalIsWedged(room_message("brigand")), + LatestEventValue::LocalCannotBeSent(room_message("brigand")), ); - buffer.push(transaction_id_2, LatestEventValue::LocalIsWedged(room_message("raclette"))); + buffer + .push(transaction_id_2, LatestEventValue::LocalCannotBeSent(room_message("raclette"))); - buffer.mark_unwedged_after(&transaction_id_1); + buffer.mark_is_sending_after(&transaction_id_1); assert_eq!(buffer.buffer.len(), 3); - assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalIsWedged(_)); - assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalIsWedged(_)); + assert_matches!(buffer.buffer[0].1, LatestEventValue::LocalCannotBeSent(_)); + assert_matches!(buffer.buffer[1].1, LatestEventValue::LocalCannotBeSent(_)); assert_matches!(buffer.buffer[2].1, LatestEventValue::LocalIsSending(_)); } } @@ -1715,7 +1719,7 @@ mod tests_latest_event_value_non_wasm { } // Receiving a `SendError` targeting the first event. The - // `LatestEventValue` must change to indicate it's wedged. + // `LatestEventValue` must change to indicate it's “cannot be sent”. { let update = RoomSendQueueUpdate::SendError { transaction_id: transaction_id_0.clone(), @@ -1724,10 +1728,10 @@ mod tests_latest_event_value_non_wasm { }; // The `LatestEventValue` has changed, it still matches the latest local - // event but it's marked as wedged. + // event but it's marked as “cannot be sent”. assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); @@ -1737,7 +1741,7 @@ mod tests_latest_event_value_non_wasm { assert_eq!(buffer.buffer.len(), 2); assert_matches!( &buffer.buffer[0].1, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); @@ -1745,7 +1749,7 @@ mod tests_latest_event_value_non_wasm { ); assert_matches!( &buffer.buffer[1].1, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); @@ -1755,7 +1759,7 @@ mod tests_latest_event_value_non_wasm { // Receiving a `SentEvent` targeting the first event. The `LatestEventValue` // must change: since an event has been sent, the following events are now - // unwedged. + // “is sending”. { let update = RoomSendQueueUpdate::SentEvent { transaction_id: transaction_id_0.clone(), @@ -1763,7 +1767,7 @@ mod tests_latest_event_value_non_wasm { }; // The `LatestEventValue` has changed, it still matches the latest local - // event but it's unwedged. + // event but it's “is sending”. assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending( @@ -1818,7 +1822,7 @@ mod tests_latest_event_value_non_wasm { } // Receiving a `SendError` targeting the first event. The - // `LatestEventValue` must change to indicate it's wedged. + // `LatestEventValue` must change to indicate it's “cannot be sent”. { let update = RoomSendQueueUpdate::SendError { transaction_id: transaction_id_0.clone(), @@ -1827,10 +1831,10 @@ mod tests_latest_event_value_non_wasm { }; // The `LatestEventValue` has changed, it still matches the latest local - // event but it's marked as wedged. + // event but it's marked as “cannot be sent”. assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); @@ -1840,7 +1844,7 @@ mod tests_latest_event_value_non_wasm { assert_eq!(buffer.buffer.len(), 2); assert_matches!( &buffer.buffer[0].1, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "A"); @@ -1848,7 +1852,7 @@ mod tests_latest_event_value_non_wasm { ); assert_matches!( &buffer.buffer[1].1, - LatestEventValue::LocalIsWedged( + LatestEventValue::LocalCannotBeSent( LatestEventContent::RoomMessage(message_content) ) => { assert_eq!(message_content.body(), "B"); @@ -1857,13 +1861,13 @@ mod tests_latest_event_value_non_wasm { } // Receiving a `RetryEvent` targeting the first event. The `LatestEventValue` - // must change: this local event and its following must be unwedged. + // must change: this local event and its following must be “is sending”. { let update = RoomSendQueueUpdate::RetryEvent { transaction_id: transaction_id_0.clone() }; // The `LatestEventValue` has changed, it still matches the latest local - // event but it's unwedged. + // event but it's “is sending”. assert_matches!( LatestEventValue::new_local(&update, &mut buffer, &room_event_cache, &None).await, LatestEventValue::LocalIsSending(