pub mod pagination;
mod state;
mod updates;
use std::{fmt, sync::Arc};
use eyeball::AsyncLock;
use matrix_sdk_base::{
event_cache::{Event, thread::ThreadInfo},
read_receipts::ReadReceipts,
sync::Timeline,
};
use ruma::{
EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, events::relation::RelationType,
room_version_rules::RoomVersionRules,
};
use tokio::sync::{Notify, broadcast::Sender, mpsc};
use tracing::{instrument, trace};
pub(in super::super) use self::state::ThreadEventCacheState;
pub(super) use self::updates::ThreadEventCacheUpdateSender;
pub use self::{pagination::ThreadPagination, updates::ThreadEventCacheUpdate};
#[cfg(feature = "e2e-encryption")]
use super::super::redecryptor::MaybeResolvedEvent;
use super::{
super::{
Result,
states::{CacheStateLock, StateLock, selectors::ThreadStateSelector},
},
EventsOrigin, TimelineVectorDiffs,
read_receipts::MaybeReceiptEventContent,
room::{RoomEventCacheGenericUpdate, RoomEventCacheLinkedChunkUpdate},
subscriber::{AutoShrinkMessage, Subscriber},
};
use crate::room::WeakRoom;
#[derive(Clone)]
pub struct ThreadEventCache {
inner: Arc<ThreadEventCacheInner>,
}
struct ThreadEventCacheInner {
room_id: OwnedRoomId,
thread_id: OwnedEventId,
weak_room: WeakRoom,
state: CacheStateLock<ThreadStateSelector>,
pagination_batch_token_notifier: Notify,
auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
update_sender: ThreadEventCacheUpdateSender,
}
impl fmt::Debug for ThreadEventCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ThreadEventCache").finish_non_exhaustive()
}
}
impl ThreadEventCache {
#[allow(clippy::too_many_arguments)]
pub(super) async fn new(
room_id: OwnedRoomId,
thread_id: OwnedEventId,
own_user_id: OwnedUserId,
room_version_rules: RoomVersionRules,
weak_room: WeakRoom,
state: &StateLock,
auto_shrink_sender: mpsc::Sender<AutoShrinkMessage>,
generic_update_sender: Sender<RoomEventCacheGenericUpdate>,
linked_chunk_update_sender: Sender<RoomEventCacheLinkedChunkUpdate>,
) -> Result<Self> {
let update_sender = ThreadEventCacheUpdateSender::new(generic_update_sender.clone());
let cache_state = state
.try_insert_once_with(
ThreadStateSelector::new(room_id.clone(), thread_id.clone()),
|store_guard| {
ThreadEventCacheState::new(
room_id.clone(),
thread_id.clone(),
weak_room.clone(),
own_user_id,
room_version_rules,
store_guard,
update_sender.clone(),
linked_chunk_update_sender,
)
},
)
.await?;
let timeline_is_not_empty =
cache_state.read().await?.thread_linked_chunk().revents().next().is_some();
let cache = Self {
inner: Arc::new(ThreadEventCacheInner {
room_id: room_id.clone(),
thread_id,
weak_room,
state: cache_state,
pagination_batch_token_notifier: Notify::new(),
auto_shrink_sender,
update_sender,
}),
};
if timeline_is_not_empty {
let _ = generic_update_sender
.send(RoomEventCacheGenericUpdate { room_id: room_id.to_owned() });
}
Ok(cache)
}
pub fn room_id(&self) -> &RoomId {
&self.inner.room_id
}
pub fn thread_id(&self) -> &EventId {
&self.inner.thread_id
}
pub async fn num_unread_messages(&self) -> Result<u64> {
Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_unread)
}
pub async fn num_unread_notifications(&self) -> Result<u64> {
Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_notifications)
}
pub async fn num_unread_mentions(&self) -> Result<u64> {
Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.num_mentions)
}
pub async fn read_receipts(&self) -> Result<ReadReceipts> {
Ok(self.inner.state.read().await?.thread_info.read().await.read_receipts.clone())
}
pub async fn subscribe_to_thread_info(
&self,
) -> Result<eyeball::Subscriber<ThreadInfo, AsyncLock>> {
Ok(self.inner.state.read().await?.thread_info.subscribe().await)
}
pub async fn subscribe(&self) -> Result<(Vec<Event>, Subscriber<ThreadEventCacheUpdate>)> {
let state = self.inner.state.read().await?;
let events =
state.thread_linked_chunk().events().map(|(_position, item)| item.clone()).collect();
let subscribers_handle = state.subscribers_handle();
let subscriber = Subscriber::new(
self.inner.update_sender.new_thread_receiver(),
AutoShrinkMessage::Thread {
room_id: self.inner.room_id.clone(),
thread_id: self.inner.thread_id.clone(),
},
self.inner.auto_shrink_sender.clone(),
subscribers_handle,
);
trace!("added a thread event cache subscriber; new count: {}", subscribers_handle.count());
Ok((events, subscriber))
}
pub fn pagination(&self) -> ThreadPagination {
ThreadPagination::new(self.inner.clone())
}
pub(in super::super) fn state(&self) -> &CacheStateLock<ThreadStateSelector> {
&self.inner.state
}
#[instrument(skip_all, fields(room_id = %self.inner.room_id, thread_root = %self.inner.thread_id))]
pub(super) async fn handle_joined_room_update(
&self,
timeline: Timeline,
read_receipts: MaybeReceiptEventContent,
) -> Result<()> {
self.handle_timeline(timeline, read_receipts).await
}
#[instrument(skip_all, fields(room_id = %self.inner.room_id, thread_root = %self.inner.thread_id))]
pub(super) async fn handle_left_room_update(&self, timeline: Timeline) -> Result<()> {
self.handle_timeline(timeline, MaybeReceiptEventContent::none()).await
}
async fn handle_timeline(
&self,
timeline: Timeline,
read_receipts: MaybeReceiptEventContent,
) -> Result<()> {
if timeline.events.is_empty() && timeline.prev_batch.is_none() && read_receipts.is_none() {
return Ok(());
}
trace!("adding new events");
let mut state = self.inner.state.write().await?;
let (stored_prev_batch_token, timeline_event_diffs) =
state.handle_sync(timeline, &read_receipts).await?;
if stored_prev_batch_token {
self.inner.pagination_batch_token_notifier.notify_one();
}
if !timeline_event_diffs.is_empty() {
state.update_sender.send(
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
diffs: timeline_event_diffs,
origin: EventsOrigin::Sync,
}),
None,
);
}
if let Some(read_receipts) = read_receipts.into_inner() {
state
.update_sender
.send(ThreadEventCacheUpdate::AddReadReceiptEvent { event: read_receipts }, None);
}
Ok(())
}
#[cfg(test)]
async fn find_event(
&self,
event_id: &EventId,
) -> Result<Option<(super::EventLocation, Event)>> {
self.inner.state.read().await?.find_event(event_id).await
}
pub async fn find_event_with_relations(
&self,
event_id: &EventId,
filter: Option<Vec<RelationType>>,
) -> Result<Option<(Event, Vec<Event>)>> {
Ok(self
.inner
.state
.read()
.await?
.find_event_with_relations(event_id, filter)
.await
.ok()
.flatten())
}
#[cfg(feature = "e2e-encryption")]
pub(in super::super) async fn replace_in_memory_utds(
&self,
resolved_events: &[MaybeResolvedEvent],
) -> Result<bool> {
let mut state = self.inner.state.write().await?;
let timeline_event_diffs = state.replace_in_memory_utds(resolved_events)?;
let _ = state.thread_linked_chunk_mut().store_updates().take();
state
.post_process_upserted_events(
resolved_events.iter().filter_map(|resolved_event| resolved_event.as_resolved()),
None,
)
.await?;
let timeline_event_diffs = timeline_event_diffs
.into_iter()
.flatten()
.chain(state.thread_linked_chunk_mut().updates_as_vector_diffs())
.collect::<Vec<_>>();
Ok(if !timeline_event_diffs.is_empty() {
state.update_sender.send(
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
diffs: timeline_event_diffs,
origin: EventsOrigin::Cache,
}),
Some(RoomEventCacheGenericUpdate { room_id: self.inner.room_id.clone() }),
);
true
} else {
false
})
}
}
#[cfg(all(test, not(target_family = "wasm")))] mod timed_tests {
use std::sync::Arc;
use assert_matches::assert_matches;
use assert_matches2::assert_let;
use eyeball_im::VectorDiff;
use futures_util::FutureExt as _;
use matrix_sdk_base::{
RoomState, ThreadingSupport,
cross_process_lock::CrossProcessLockConfig,
event_cache::{
Gap,
store::{EventCacheStore as _, MemoryStore},
},
linked_chunk::{
ChunkContent, ChunkIdentifier, LinkedChunkId, Position, Update,
lazy_loader::from_all_chunks,
},
store::StoreConfig,
sync::Timeline,
};
use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
use ruma::{
event_id,
events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent},
room_id, user_id,
};
use tokio::task::yield_now;
use super::{
super::{super::RoomEventCacheGenericUpdate, TimelineVectorDiffs},
MaybeReceiptEventContent, ThreadEventCacheUpdate,
};
use crate::{assert_let_timeout, test_utils::client::MockClientBuilder};
#[async_test]
async fn test_write_to_storage() {
let room_id = room_id!("!r0");
let thread_root = event_id!("$t0_ev0");
let thread_event_id_0 = event_id!("$t0_ev1");
let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
let event_cache_store = Arc::new(MemoryStore::new());
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_root).await.unwrap();
let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
assert!(thread_events.is_empty());
let timeline = Timeline {
limited: true,
prev_batch: Some("raclette".to_owned()),
events: vec![
f.text_msg("salut")
.event_id(thread_event_id_0)
.in_thread(thread_root, thread_root)
.into_event(),
],
};
thread_event_cache
.handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
.await
.unwrap();
assert_matches!(
thread_stream.recv().await,
Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Clear);
assert_matches!(&diffs[1], VectorDiff::Append { values: events } => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id(), Some(thread_event_id_0));
});
}
);
assert!(thread_stream.is_empty());
let linked_chunk = from_all_chunks::<3, _, _>(
event_cache_store
.load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
.await
.unwrap(),
)
.unwrap()
.unwrap();
assert_eq!(linked_chunk.chunks().count(), 2);
let mut chunks = linked_chunk.chunks();
assert_matches!(chunks.next().unwrap().content(), ChunkContent::Gap(gap) => {
assert_eq!(gap.token, "raclette");
});
assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id(), Some(thread_event_id_0));
});
assert!(chunks.next().is_none());
}
#[async_test]
async fn test_write_to_storage_strips_bundled_relations() {
let sender = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!r0");
let thread_root = event_id!("$t0_ev0");
let thread_event_id_0 = event_id!("$t0_ev1");
let f = EventFactory::new().room(room_id).sender(sender);
let event_cache_store = Arc::new(MemoryStore::new());
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_root).await.unwrap();
let timeline = Timeline {
limited: false,
prev_batch: None,
events: vec![
f.text_msg("s 'up")
.event_id(thread_event_id_0)
.with_bundled_edit(f.text_msg("Hello, Kind Sir").sender(sender))
.in_thread(thread_root, thread_root)
.into_event(),
],
};
thread_event_cache
.handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
.await
.unwrap();
{
let (events, _) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(events.len(), 1);
let event = events[0].raw().deserialize().unwrap();
assert_eq!(event.event_id(), thread_event_id_0);
assert_let!(
AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) =
event
);
assert!(msg.as_original().unwrap().unsigned.relations.replace.is_some());
}
let linked_chunk = from_all_chunks::<3, _, _>(
event_cache_store
.load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
.await
.unwrap(),
)
.unwrap()
.unwrap();
assert_eq!(linked_chunk.chunks().count(), 1);
let mut chunks = linked_chunk.chunks();
assert_matches!(chunks.next().unwrap().content(), ChunkContent::Items(events) => {
assert_eq!(events.len(), 1);
let event = events[0].raw().deserialize().unwrap();
assert_eq!(event.event_id(), thread_event_id_0);
assert_let!(AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(msg)) = event);
assert!(msg.as_original().unwrap().unsigned.relations.replace.is_none());
});
assert!(chunks.next().is_none());
}
#[async_test]
async fn test_clear() {
let room_id = room_id!("!r0");
let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
let event_cache_store = Arc::new(MemoryStore::new());
let thread_root = event_id!("$t0_ev0");
let thread_event_id_0 = event_id!("$t0_ev1");
let thread_event_id_1 = event_id!("$t0_ev2");
let thread_event_0 = f
.text_msg("foo")
.event_id(thread_event_id_0)
.in_thread(thread_root, thread_root)
.into_event();
let thread_event_1 = f
.text_msg("bar")
.event_id(thread_event_id_1)
.in_thread(thread_root, thread_event_id_0)
.into_event();
event_cache_store
.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_root),
vec![
Update::NewItemsChunk {
previous: None,
new: ChunkIdentifier::new(0),
next: None,
},
Update::NewGapChunk {
previous: Some(ChunkIdentifier::new(0)),
new: ChunkIdentifier::new(42),
next: None,
gap: Gap { token: "comté".to_owned() },
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(42)),
new: ChunkIdentifier::new(1),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(1), 0),
items: vec![thread_event_0.clone()],
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(1)),
new: ChunkIdentifier::new(2),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(2), 0),
items: vec![thread_event_1.clone()],
},
],
)
.await
.unwrap();
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_root).await.unwrap();
let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
{
assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_some());
assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_some());
}
{
assert_eq!(thread_events.len(), 1);
assert_eq!(thread_events[0].event_id().unwrap(), thread_event_id_1);
assert!(thread_stream.is_empty());
}
{
thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
assert_matches!(
thread_stream.recv().await,
Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
assert_eq!(diffs.len(), 1);
assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
});
}
);
assert!(thread_stream.is_empty());
assert_matches!(
generic_stream.recv().await,
Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
assert_eq!(room_id, expected_room_id);
}
);
assert!(generic_stream.is_empty());
}
event_cache.clear_all_rooms().await.unwrap();
assert_matches!(
thread_stream.recv().await,
Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
assert_eq!(diffs.len(), 1);
assert_matches!(&diffs[0], VectorDiff::Clear);
}
);
assert_matches!(
generic_stream.recv().await,
Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
assert_eq!(received_room_id, room_id);
}
);
assert_matches!(
generic_stream.recv().await,
Ok(RoomEventCacheGenericUpdate { room_id: received_room_id }) => {
assert_eq!(received_room_id, room_id);
}
);
assert!(generic_stream.is_empty());
assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_none());
assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_none());
let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
assert!(thread_events.is_empty());
assert!(
event_cache_store
.load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
.await
.unwrap()
.is_empty()
);
}
#[async_test]
async fn test_load_from_storage() {
let room_id = room_id!("!r0");
let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
let event_cache_store = Arc::new(MemoryStore::new());
let thread_root = event_id!("$t0");
let thread_event_id_0 = event_id!("$t0_ev0");
let thread_event_id_1 = event_id!("$t0_ev1");
let thread_event_0 = f
.text_msg("hello world")
.event_id(thread_event_id_0)
.in_thread(thread_root, thread_root)
.into_event();
let thread_event_1 = f
.text_msg("how's it going")
.event_id(thread_event_id_1)
.in_thread(thread_root, thread_event_id_1)
.into_event();
let updates = vec![
Update::NewItemsChunk { previous: None, new: ChunkIdentifier::new(0), next: None },
Update::NewGapChunk {
previous: Some(ChunkIdentifier::new(0)),
new: ChunkIdentifier::new(42),
next: None,
gap: Gap { token: "gruyère".to_owned() },
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(42)),
new: ChunkIdentifier::new(1),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(1), 0),
items: vec![thread_event_0.clone()],
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(1)),
new: ChunkIdentifier::new(2),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(2), 0),
items: vec![thread_event_1.clone()],
},
];
event_cache_store
.handle_linked_chunk_updates(LinkedChunkId::Room(room_id), updates.clone())
.await
.unwrap();
event_cache_store
.handle_linked_chunk_updates(LinkedChunkId::Thread(room_id, thread_root), updates)
.await
.unwrap();
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("hodor"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_root).await.unwrap();
let (thread_events, mut thread_stream) = thread_event_cache.subscribe().await.unwrap();
for _ in 0..2 {
assert_matches!(
generic_stream.recv().await,
Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
assert_eq!(room_id, expected_room_id);
}
);
}
assert!(generic_stream.is_empty());
assert_eq!(thread_events.len(), 1);
assert_eq!(thread_events[0].event_id().unwrap(), thread_event_id_1);
assert!(thread_stream.is_empty());
assert!(thread_event_cache.find_event(thread_event_id_0).await.unwrap().is_some());
assert!(thread_event_cache.find_event(thread_event_id_1).await.unwrap().is_some());
thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
assert_matches!(
thread_stream.recv().await,
Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) => {
assert_eq!(diffs.len(), 1);
assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
});
}
);
assert!(thread_stream.is_empty());
assert_matches!(
generic_stream.recv().await,
Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) => {
assert_eq!(expected_room_id, room_id);
}
);
assert!(generic_stream.is_empty());
let timeline = Timeline { limited: false, prev_batch: None, events: vec![thread_event_1] };
thread_event_cache
.handle_joined_room_update(timeline, MaybeReceiptEventContent::none())
.await
.unwrap();
assert!(generic_stream.recv().now_or_never().is_none());
let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(thread_events.len(), 2);
assert_eq!(thread_events[0].event_id(), Some(thread_event_id_0));
assert_eq!(thread_events[1].event_id(), Some(thread_event_id_1));
}
#[async_test]
async fn test_load_from_storage_resilient_to_failure() {
let room_id = room_id!("!r0");
let f = EventFactory::new().room(room_id).sender(user_id!("@mnt_io:matrix.org"));
let event_cache_store = Arc::new(MemoryStore::new());
let thread_root = event_id!("$t0");
let thread_event_id_0 = event_id!("$t0_ev0");
let thread_event_0 = f
.text_msg("hello world")
.event_id(thread_event_id_0)
.in_thread(thread_root, thread_root)
.into_event();
event_cache_store
.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_root),
vec![
Update::NewItemsChunk {
previous: None,
new: ChunkIdentifier::new(0),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(0), 0),
items: vec![thread_event_0],
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(0)),
new: ChunkIdentifier::new(1),
next: Some(ChunkIdentifier::new(0)),
},
],
)
.await
.unwrap();
let client = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("holder"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_root).await.unwrap();
let (thread_events, _) = thread_event_cache.subscribe().await.unwrap();
assert!(thread_events.is_empty());
let raw_chunks = event_cache_store
.load_all_chunks(LinkedChunkId::Thread(room_id, thread_root))
.await
.unwrap();
assert!(raw_chunks.is_empty());
}
#[async_test]
async fn test_reload_when_dirty() {
let user_id = user_id!("@mnt_io:matrix.org");
let room_id = room_id!("!raclette:patate.ch");
let event_cache_store = MemoryStore::new();
let client_p0 = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("process #0"))
.event_cache_store(event_cache_store.clone()),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let client_p1 = MockClientBuilder::new(None)
.on_builder(|builder| {
builder
.store_config(
StoreConfig::new(CrossProcessLockConfig::multi_process("process #1"))
.event_cache_store(event_cache_store),
)
.with_threading_support(ThreadingSupport::Enabled { with_subscriptions: true })
})
.build()
.await;
let event_factory = EventFactory::new().room(room_id).sender(user_id);
let thread_root = event_id!("$t0");
let thread_event_id_0 = event_id!("$t0_ev0");
let thread_event_id_1 = event_id!("$t0_ev1");
let thread_event_0 = event_factory
.text_msg("comté")
.event_id(thread_event_id_0)
.in_thread(thread_root, thread_root)
.into_event();
let thread_event_1 = event_factory
.text_msg("morbier")
.event_id(thread_event_id_1)
.in_thread(thread_root, thread_event_id_0)
.into_event();
client_p0
.event_cache_store()
.lock()
.await
.expect("[p0] Could not acquire the event cache lock")
.as_clean()
.expect("[p0] Could not acquire a clean event cache lock")
.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_root),
vec![
Update::NewItemsChunk {
previous: None,
new: ChunkIdentifier::new(0),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(0), 0),
items: vec![thread_event_0],
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(0)),
new: ChunkIdentifier::new(1),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(1), 0),
items: vec![thread_event_1],
},
],
)
.await
.unwrap();
let (thread_event_cache_p0, thread_event_cache_p1) = {
let event_cache_p0 = client_p0.event_cache();
event_cache_p0.subscribe().unwrap();
let event_cache_p1 = client_p1.event_cache();
event_cache_p1.subscribe().unwrap();
client_p0.base_client().get_or_create_room(room_id, RoomState::Joined);
client_p1.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache_p0, _drop_handles) =
event_cache_p0.thread(room_id, thread_root).await.unwrap();
let (thread_event_cache_p1, _drop_handles) =
event_cache_p1.thread(room_id, thread_root).await.unwrap();
(thread_event_cache_p0, thread_event_cache_p1)
};
let mut updates_stream_p0 = {
let thread_event_cache = &thread_event_cache_p0;
let (initial_updates, mut updates_stream) =
thread_event_cache_p0.subscribe().await.unwrap();
assert_eq!(initial_updates.len(), 1);
assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
assert!(updates_stream.is_empty());
thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 1, "{diffs:#?}");
assert_matches!(
&diffs[0],
VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
}
);
}
);
updates_stream
};
let mut updates_stream_p1 = {
let thread_event_cache = &thread_event_cache_p1;
let (initial_updates, mut updates_stream) =
thread_event_cache_p1.subscribe().await.unwrap();
assert_eq!(initial_updates.len(), 1);
assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
assert!(updates_stream.is_empty());
thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 1, "{diffs:#?}");
assert_matches!(
&diffs[0],
VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
}
);
}
);
updates_stream
};
for _ in 0..3 {
{
let thread_event_cache = &thread_event_cache_p0;
let updates_stream = &mut updates_stream_p0;
let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(initial_updates.len(), 1);
assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 2, "{diffs:#?}");
assert_matches!(&diffs[0], VectorDiff::Clear);
assert_matches!(
&diffs[1],
VectorDiff::Append { values: events } => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id(), Some(thread_event_id_1));
}
);
}
);
thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 1, "{diffs:#?}");
assert_matches!(
&diffs[0],
VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
}
);
}
);
}
{
let thread_event_cache = &thread_event_cache_p1;
let updates_stream = &mut updates_stream_p1;
let (initial_updates, _) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(initial_updates.len(), 1);
assert_eq!(initial_updates[0].event_id(), Some(thread_event_id_1));
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 2, "{diffs:#?}");
assert_matches!(&diffs[0], VectorDiff::Clear);
assert_matches!(
&diffs[1],
VectorDiff::Append { values: events } => {
assert_eq!(events.len(), 1);
assert_eq!(events[0].event_id(), Some(thread_event_id_1));
}
);
}
);
thread_event_cache.pagination().run_backwards_once(1).await.unwrap();
assert_matches!(
updates_stream.recv().await.unwrap(),
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. }) => {
assert_eq!(diffs.len(), 1, "{diffs:#?}");
assert_matches!(
&diffs[0],
VectorDiff::Insert { index: 0, value: event } => {
assert_eq!(event.event_id(), Some(thread_event_id_0));
}
);
}
);
}
}
}
#[async_test]
async fn test_auto_shrink_after_all_subscribers_are_gone() {
let room_id = room_id!("!r0");
let thread_id = event_id!("$t0");
let client = MockClientBuilder::new(None).build().await;
let f = EventFactory::new().room(room_id).sender(*ALICE);
let event_id_0 = event_id!("$ev0");
let event_id_1 = event_id!("$ev1");
let thread_root =
f.text_msg("gr00t").event_id(thread_id).in_thread(thread_id, thread_id).into_event();
let event_0 =
f.text_msg("hello").event_id(event_id_0).in_thread(thread_id, event_id_0).into_event();
let event_1 =
f.text_msg("world").event_id(event_id_1).in_thread(thread_id, event_id_1).into_event();
{
client
.event_cache_store()
.lock()
.await
.expect("Could not acquire the event cache lock")
.as_clean()
.expect("Could not acquire a clean event cache lock")
.handle_linked_chunk_updates(
LinkedChunkId::Thread(room_id, thread_id),
vec![
Update::NewItemsChunk {
previous: None,
new: ChunkIdentifier::new(0),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(0), 0),
items: vec![thread_root, event_0],
},
Update::NewItemsChunk {
previous: Some(ChunkIdentifier::new(0)),
new: ChunkIdentifier::new(1),
next: None,
},
Update::PushItems {
at: Position::new(ChunkIdentifier::new(1), 0),
items: vec![event_1],
},
],
)
.await
.unwrap();
}
let event_cache = client.event_cache();
event_cache.subscribe().unwrap();
client.base_client().get_or_create_room(room_id, RoomState::Joined);
let (thread_event_cache, _drop_handles) =
event_cache.thread(room_id, thread_id).await.unwrap();
let (events1, mut stream1) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(events1.len(), 1);
assert_eq!(events1[0].event_id(), Some(event_id_1));
assert!(stream1.is_empty());
let mut generic_stream = event_cache.subscribe_to_room_generic_updates();
let outcome = thread_event_cache.pagination().run_backwards_once(20).await.unwrap();
assert_eq!(outcome.events.len(), 2);
assert_eq!(outcome.events[0].event_id(), Some(event_id_0));
assert_eq!(outcome.events[1].event_id(), Some(thread_id));
assert!(outcome.reached_start);
assert_let_timeout!(
Ok(ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs { diffs, .. })) =
stream1.recv()
);
assert_eq!(diffs.len(), 2);
assert_matches!(&diffs[0], VectorDiff::Insert { index: 0, value } => {
assert_eq!(value.event_id(), Some(thread_id));
});
assert_matches!(&diffs[1], VectorDiff::Insert { index: 1, value } => {
assert_eq!(value.event_id(), Some(event_id_0));
});
assert!(stream1.is_empty());
assert_let_timeout!(
Ok(RoomEventCacheGenericUpdate { room_id: expected_room_id }) = generic_stream.recv()
);
assert_eq!(expected_room_id, room_id);
assert!(generic_stream.is_empty());
let (events2, stream2) = thread_event_cache.subscribe().await.unwrap();
assert_eq!(events2.len(), 3);
assert_eq!(events2[0].event_id(), Some(thread_id));
assert_eq!(events2[1].event_id(), Some(event_id_0));
assert_eq!(events2[2].event_id(), Some(event_id_1));
assert!(stream2.is_empty());
let subscriber = {
let state = thread_event_cache.inner.state.read().await.unwrap();
state.update_sender.new_thread_receiver()
};
drop(stream1);
yield_now().await;
assert!(stream2.is_empty());
drop(stream2);
yield_now().await;
{
let state = thread_event_cache.inner.state.read().await.unwrap();
assert_eq!(state.subscribers_handle().count(), 0);
assert!(subscriber.is_empty());
assert!(generic_stream.is_empty());
}
let events3 = thread_event_cache
.inner
.state
.read()
.await
.unwrap()
.thread_linked_chunk()
.events()
.map(|(_position, item)| item.clone())
.collect::<Vec<_>>();
assert_eq!(events3.len(), 1);
assert_eq!(events3[0].event_id(), Some(event_id_1));
}
}