use std::collections::{HashMap, HashSet};
use matrix_sdk_base::{
deserialized_responses::TimelineEventKind,
event_cache::{Event, Gap, store::EventCacheStoreLockGuard},
executor::spawn,
linked_chunk::{ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Update},
};
use ruma::{EventId, RoomId, events::relation::RelationType, serde::Raw};
use tokio::sync::broadcast::Sender;
use tracing::trace;
use super::{
EventCacheError, Result,
caches::{
EventLocation, event_linked_chunk::EventLinkedChunk, room::RoomEventCacheLinkedChunkUpdate,
},
};
pub(super) async fn load_linked_chunk_metadata(
store_guard: &EventCacheStoreLockGuard,
linked_chunk_id: LinkedChunkId<'_>,
) -> Result<Option<Vec<ChunkMetadata>>> {
let mut all_chunks = store_guard
.load_all_chunks_metadata(linked_chunk_id)
.await
.map_err(EventCacheError::from)?;
if all_chunks.is_empty() {
return Ok(None);
}
let chunk_map: HashMap<_, _> = all_chunks.iter().map(|meta| (meta.identifier, meta)).collect();
let mut iter = all_chunks.iter().filter(|meta| meta.next.is_none());
let Some(last) = iter.next() else {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: "no last chunk found".to_owned(),
});
};
if let Some(other_last) = iter.next() {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: format!(
"chunks {} and {} both claim to be last chunks",
last.identifier.index(),
other_last.identifier.index()
),
});
}
let mut seen = HashSet::new();
let mut current = last;
loop {
if !seen.insert(current.identifier) {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: format!(
"cycle detected in linked chunk at {}",
current.identifier.index()
),
});
}
let Some(prev_id) = current.previous else {
if seen.len() != all_chunks.len() {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: format!(
"linked chunk likely has multiple components: {} chunks seen through the chain of predecessors, but {} expected",
seen.len(),
all_chunks.len()
),
});
}
break;
};
let Some(pred_meta) = chunk_map.get(&prev_id) else {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: format!(
"missing predecessor {} chunk for {}",
prev_id.index(),
current.identifier.index()
),
});
};
if pred_meta.next != Some(current.identifier) {
return Err(EventCacheError::InvalidLinkedChunkMetadata {
details: format!(
"chunk {}'s next ({:?}) doesn't match the current chunk ({})",
pred_meta.identifier.index(),
pred_meta.next.map(|chunk_id| chunk_id.index()),
current.identifier.index()
),
});
}
current = *pred_meta;
}
let mut current = current.identifier;
for i in 0..all_chunks.len() {
let j = all_chunks
.iter()
.rev()
.position(|meta| meta.identifier == current)
.map(|j| all_chunks.len() - 1 - j)
.expect("the target chunk must be present in the metadata");
if i != j {
all_chunks.swap(i, j);
}
if let Some(next) = all_chunks[i].next {
current = next;
}
}
Ok(Some(all_chunks))
}
pub(super) async fn send_updates_to_store(
store: &EventCacheStoreLockGuard,
linked_chunk_id: OwnedLinkedChunkId,
linked_chunk_update_sender: &Sender<RoomEventCacheLinkedChunkUpdate>,
mut updates: Vec<Update<Event, Gap>>,
) -> Result<()> {
if updates.is_empty() {
return Ok(());
}
for update in updates.iter_mut() {
match update {
Update::PushItems { items, .. } => strip_relations_from_events(items),
Update::ReplaceItem { item, .. } => strip_relations_from_event(item),
Update::NewItemsChunk { .. }
| Update::NewGapChunk { .. }
| Update::RemoveChunk(_)
| Update::RemoveItem { .. }
| Update::DetachLastItems { .. }
| Update::StartReattachItems
| Update::EndReattachItems
| Update::Clear => {}
}
}
let store = store.clone();
let cloned_updates = updates.clone();
let cloned_linked_chunk_id = linked_chunk_id.clone();
spawn(async move {
trace!(updates = ?cloned_updates, "sending linked chunk updates to the store");
store.handle_linked_chunk_updates(cloned_linked_chunk_id.as_ref(), cloned_updates).await?;
trace!("linked chunk updates applied");
Result::Ok(())
})
.await
.expect("joining failed")?;
let _ = linked_chunk_update_sender
.send(RoomEventCacheLinkedChunkUpdate { linked_chunk_id, updates });
Ok(())
}
fn strip_relations_from_events(items: &mut [Event]) {
for ev in items.iter_mut() {
strip_relations_from_event(ev);
}
}
fn strip_relations_from_event(ev: &mut Event) {
match &mut ev.kind {
TimelineEventKind::Decrypted(decrypted) => {
decrypted.unsigned_encryption_info = None;
strip_relations_if_present(&mut decrypted.event);
}
TimelineEventKind::UnableToDecrypt { event, .. }
| TimelineEventKind::PlainText { event } => {
strip_relations_if_present(event);
}
}
}
fn strip_relations_if_present<T>(event: &mut Raw<T>) {
let mut closure = || -> Option<()> {
let mut val: serde_json::Value = event.deserialize_as().ok()?;
let unsigned = val.get_mut("unsigned")?;
let unsigned_obj = unsigned.as_object_mut()?;
if unsigned_obj.remove("m.relations").is_some() {
*event = Raw::new(&val).ok()?.cast_unchecked();
}
None
};
let _ = closure();
}
pub async fn find_event(
event_id: &EventId,
room_id: &RoomId,
event_linked_chunk: &EventLinkedChunk,
store: &EventCacheStoreLockGuard,
) -> Result<Option<(EventLocation, Event)>> {
for (position, event) in event_linked_chunk.revents() {
if event.event_id() == Some(event_id) {
return Ok(Some((EventLocation::Memory(position), event.clone())));
}
}
Ok(store.find_event(room_id, event_id).await?.map(|event| (EventLocation::Store, event)))
}
pub async fn find_event_with_relations(
event_id: &EventId,
room_id: &RoomId,
filters: Option<Vec<RelationType>>,
event_linked_chunk: &EventLinkedChunk,
store: &EventCacheStoreLockGuard,
) -> Result<Option<(Event, Vec<Event>)>> {
let found = store.find_event(room_id, event_id).await?;
let Some(target) = found else {
return Ok(None);
};
let related =
find_event_relations(event_id, room_id, filters, event_linked_chunk, store).await?;
Ok(Some((target, related)))
}
pub async fn find_event_relations(
event_id: &EventId,
room_id: &RoomId,
filters: Option<Vec<RelationType>>,
event_linked_chunk: &EventLinkedChunk,
store: &EventCacheStoreLockGuard,
) -> Result<Vec<Event>> {
let mut related = store.find_event_relations(room_id, event_id, filters.as_deref()).await?;
let mut stack = related
.iter()
.filter_map(|(event, _pos)| event.event_id().map(ToOwned::to_owned))
.collect::<Vec<_>>();
let mut already_seen = HashSet::new();
already_seen.insert(event_id.to_owned());
let mut num_iters = 1;
while let Some(event_id) = stack.pop() {
if !already_seen.insert(event_id.clone()) {
continue;
}
let other_related =
store.find_event_relations(room_id, &event_id, filters.as_deref()).await?;
stack.extend(
other_related
.iter()
.filter_map(|(event, _pos)| event.event_id().map(ToOwned::to_owned)),
);
related.extend(other_related);
num_iters += 1;
}
trace!(num_related = %related.len(), num_iters, "computed transitive closure of related events");
related.sort_by(|(_, lhs), (_, rhs)| {
use std::cmp::Ordering;
match (lhs, rhs) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(lhs), Some(rhs)) => {
let lhs = event_linked_chunk.event_order(*lhs);
let rhs = event_linked_chunk.event_order(*rhs);
match (lhs, rhs) {
(None, None) => Ordering::Equal,
(None, Some(_)) => Ordering::Less,
(Some(_), None) => Ordering::Greater,
(Some(lhs), Some(rhs)) => lhs.cmp(&rhs),
}
}
}
});
let related = related.into_iter().map(|(event, _pos)| event).collect();
Ok(related)
}