use std::{fmt, sync::Arc};
use eyeball::SharedObservable;
use eyeball_im::VectorDiff;
use matrix_sdk_base::{
event_cache::{Event, Gap},
linked_chunk::{ChunkContent, LinkedChunkId, Update},
};
use ruma::api::Direction;
use tracing::{error, trace};
use super::{
super::{
super::{
EventCacheError, EventsOrigin, Result, TimelineVectorDiffs,
deduplicator::{DeduplicationOutcome, filter_duplicate_events},
},
pagination::{
BackPaginationOutcome, LoadMoreEventsBackwardsOutcome, PaginatedCache, Pagination,
SharedPaginationStatus,
},
room::RoomEventCacheGenericUpdate,
},
ThreadEventCacheInner,
updates::ThreadEventCacheUpdate,
};
use crate::room::{IncludeRelations, RelationsOptions};
#[derive(Clone)]
struct ThreadEventCacheWrapper {
cache: Arc<ThreadEventCacheInner>,
dummy_pagination_status: SharedObservable<SharedPaginationStatus>,
}
#[allow(missing_debug_implementations)]
pub struct ThreadPagination(Pagination<ThreadEventCacheWrapper>);
impl ThreadPagination {
pub(super) fn new(cache: Arc<ThreadEventCacheInner>) -> Self {
Self(Pagination::new(ThreadEventCacheWrapper {
cache,
dummy_pagination_status: SharedObservable::new(SharedPaginationStatus::Idle {
hit_timeline_start: false,
}),
}))
}
pub async fn run_backwards_until(
&self,
num_requested_events: u16,
) -> Result<BackPaginationOutcome> {
self.0.run_backwards_until(num_requested_events).await
}
pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
self.0.run_backwards_once(batch_size).await
}
}
impl PaginatedCache for ThreadEventCacheWrapper {
fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
&self.dummy_pagination_status
}
async fn load_more_events_backwards(&self) -> Result<LoadMoreEventsBackwardsOutcome> {
let mut state = self.cache.state.write().await?;
if let Some(prev_token) = state.thread_linked_chunk().rgap().map(|gap| gap.token) {
trace!(%prev_token, "thread chunk has at least a gap");
return Ok(LoadMoreEventsBackwardsOutcome::Gap {
prev_token: Some(prev_token),
waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
});
}
let prev_first_chunk = state.thread_linked_chunk().first_chunk();
let linked_chunk_id = LinkedChunkId::Thread(&state.room_id, &state.thread_id);
let new_first_chunk = match state
.store
.load_previous_chunk(linked_chunk_id, prev_first_chunk.identifier())
.await
{
Ok(Some(new_first_chunk)) => {
new_first_chunk
}
Ok(None) => {
if let Some((_pos, first_event)) = state.thread_linked_chunk().events().next()
&& self.cache.thread_id
== first_event.event_id().expect("Stored events all have an ID")
{
trace!("thread chunk is fully loaded and non-empty: reached_start=true");
return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
}
return Ok(LoadMoreEventsBackwardsOutcome::Gap {
prev_token: None,
waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
});
}
Err(err) => {
error!("error when loading the previous chunk of a linked chunk: {err}");
state
.store
.handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
.await?;
return Err(err.into());
}
};
let chunk_content = new_first_chunk.content.clone();
let reached_start = new_first_chunk.previous.is_none();
if let Err(err) = state.thread_linked_chunk_mut().insert_new_chunk_as_first(new_first_chunk)
{
error!("error when inserting the previous chunk into its linked chunk: {err}");
state
.store
.handle_linked_chunk_updates(
LinkedChunkId::Thread(&state.room_id, &state.thread_id),
vec![Update::Clear],
)
.await?;
return Err(err.into());
}
let _ = state.thread_linked_chunk_mut().store_updates().take();
let timeline_event_diffs = state.thread_linked_chunk_mut().updates_as_vector_diffs();
Ok(match chunk_content {
ChunkContent::Gap(gap) => {
trace!("reloaded chunk from disk (gap)");
LoadMoreEventsBackwardsOutcome::Gap {
prev_token: Some(gap.token),
waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
}
}
ChunkContent::Items(events) => {
trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());
LoadMoreEventsBackwardsOutcome::Events {
events,
timeline_event_diffs,
reached_start,
}
}
})
}
async fn mark_has_waited_for_initial_prev_token(&self) -> Result<()> {
*self.cache.state.write().await?.waited_for_initial_prev_token_mut() = true;
Ok(())
}
async fn wait_for_prev_token(&self) {
self.cache.pagination_batch_token_notifier.notified().await
}
async fn paginate_backwards_with_network(
&self,
batch_size: u16,
prev_token: &Option<String>,
) -> Result<Option<(Vec<Event>, Option<String>)>> {
let Some(room) = self.cache.weak_room.get() else {
return Ok(None);
};
let options = RelationsOptions {
from: prev_token.clone(),
dir: Direction::Backward,
limit: Some(batch_size.into()),
include_relations: IncludeRelations::AllRelations,
recurse: true,
};
let response = room
.relations(self.cache.thread_id.clone(), options)
.await
.map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
Ok(Some((response.chunk, response.next_batch_token)))
}
async fn conclude_backwards_pagination_from_disk(
&self,
events: Vec<Event>,
timeline_event_diffs: Vec<VectorDiff<Event>>,
reached_start: bool,
) -> BackPaginationOutcome {
if !timeline_event_diffs.is_empty() {
self.cache.update_sender.send(
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
diffs: timeline_event_diffs,
origin: EventsOrigin::Cache,
}),
Some(RoomEventCacheGenericUpdate { room_id: self.cache.room_id.clone() }),
);
}
BackPaginationOutcome {
reached_start,
events: events.into_iter().rev().collect(),
}
}
async fn conclude_backwards_pagination_from_network(
&self,
mut events: Vec<Event>,
prev_token: Option<String>,
mut new_token: Option<String>,
) -> Result<Option<BackPaginationOutcome>> {
let Some(room) = self.cache.weak_room.get() else {
return Ok(None);
};
if new_token.is_none() {
events.push(
room.load_or_fetch_event(&self.cache.thread_id, None)
.await
.map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?,
);
}
let mut state = self.cache.state.write().await?;
let prev_gap_id = if let Some(token) = prev_token {
let gap_chunk_id = state.thread_linked_chunk().chunk_identifier(|chunk| {
matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)
});
if gap_chunk_id.is_none() {
return Ok(None);
}
gap_chunk_id
} else {
None
};
let DeduplicationOutcome {
all_events: mut events,
in_memory_duplicated_event_ids,
in_store_duplicated_event_ids,
non_empty_all_duplicates: all_duplicates,
} = filter_duplicate_events(
&state.own_user_id,
&state.store,
LinkedChunkId::Thread(&state.room_id, &state.thread_id),
state.thread_linked_chunk(),
events,
)
.await?;
if !all_duplicates {
state
.remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids)
.await?;
} else {
events.clear();
new_token = None;
}
let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();
let new_gap = new_token.map(|prev_token| Gap { token: prev_token });
let reached_start = state.thread_linked_chunk_mut().push_backwards_pagination_events(
prev_gap_id,
new_gap,
&topo_ordered_events,
);
state.state.propagate_changes(&state.store).await?;
let receipt_event = None;
state.post_process_upserted_events(topo_ordered_events.iter(), receipt_event).await?;
let timeline_event_diffs = state.thread_linked_chunk_mut().updates_as_vector_diffs();
if !timeline_event_diffs.is_empty() {
state.update_sender.send(
ThreadEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
diffs: timeline_event_diffs,
origin: EventsOrigin::Pagination,
}),
Some(RoomEventCacheGenericUpdate { room_id: state.room_id.clone() }),
);
}
Ok(Some(BackPaginationOutcome { reached_start, events }))
}
}
impl fmt::Debug for ThreadPagination {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("ThreadPagination").finish_non_exhaustive()
}
}