matrix_sdk/event_cache/caches/room/
pagination.rs1use std::{
21 fmt,
22 pin::Pin,
23 sync::Arc,
24 task::{Context, Poll},
25};
26
27use eyeball::{SharedObservable, Subscriber};
28use eyeball_im::VectorDiff;
29use futures_core::{Stream, ready};
30use matrix_sdk_base::{
31 event_cache::{Event, Gap},
32 linked_chunk::{ChunkContent, LinkedChunkId, Update},
33};
34use pin_project_lite::pin_project;
35use ruma::api::Direction;
36use tracing::{error, trace};
37
38pub use super::super::pagination::PaginationStatus;
39use super::{
40 super::{
41 super::{
42 EventCacheError, EventsOrigin, Result, RoomEventCacheGenericUpdate,
43 deduplicator::{DeduplicationOutcome, filter_duplicate_events},
44 },
45 TimelineVectorDiffs,
46 pagination::{
47 BackPaginationOutcome, LoadMoreEventsBackwardsOutcome, PaginatedCache, Pagination,
48 },
49 read_receipts::{contains_a_receipt_target, unresolved_receipt_targets},
50 },
51 RoomEventCacheInner, RoomEventCacheUpdate,
52};
53use crate::{event_cache::caches::pagination::SharedPaginationStatus, room::MessagesOptions};
54
55pin_project! {
56 pub struct PaginationStatusSubscriber {
61 #[pin]
62 subscriber: Subscriber<SharedPaginationStatus>,
63 }
64}
65
66#[cfg(not(tarpaulin_include))]
67impl fmt::Debug for PaginationStatusSubscriber {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.debug_struct("PaginationStatusSubscriber").finish_non_exhaustive()
70 }
71}
72
73impl PaginationStatusSubscriber {
74 fn map(from: SharedPaginationStatus) -> PaginationStatus {
75 match from {
76 SharedPaginationStatus::Idle { hit_timeline_start } => {
77 PaginationStatus::Idle { hit_timeline_start }
78 }
79 SharedPaginationStatus::Paginating { .. } => PaginationStatus::Paginating,
80 }
81 }
82
83 pub fn get(&self) -> PaginationStatus {
84 Self::map(self.subscriber.get())
85 }
86
87 pub async fn next(&mut self) -> Option<PaginationStatus> {
88 self.subscriber.next().await.map(Self::map)
89 }
90
91 pub fn next_now(&mut self) -> PaginationStatus {
92 Self::map(self.subscriber.next_now())
93 }
94}
95
96impl Stream for PaginationStatusSubscriber {
97 type Item = PaginationStatus;
98
99 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
100 Poll::Ready(ready!(self.project().subscriber.as_mut().poll_next(cx)).map(Self::map))
101 }
102}
103
104#[allow(missing_debug_implementations)]
111#[derive(Clone)]
112pub struct RoomPagination(Pagination<Arc<RoomEventCacheInner>>);
113
114impl RoomPagination {
115 pub(super) fn new(cache: Arc<RoomEventCacheInner>) -> Self {
117 Self(Pagination::new(cache))
118 }
119
120 pub async fn run_backwards_until(
132 &self,
133 num_requested_events: u16,
134 ) -> Result<BackPaginationOutcome> {
135 self.0.run_backwards_until(num_requested_events).await
136 }
137
138 pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
143 self.0.run_backwards_once(batch_size).await
144 }
145
146 pub fn status(&self) -> PaginationStatusSubscriber {
148 PaginationStatusSubscriber { subscriber: self.0.cache.status().subscribe() }
149 }
150
151 #[cfg(test)]
152 pub(super) async fn load_more_events_backwards(
153 &self,
154 ) -> Result<LoadMoreEventsBackwardsOutcome> {
155 self.0.cache.load_more_events_backwards().await
156 }
157}
158
159impl PaginatedCache for Arc<RoomEventCacheInner> {
160 fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
161 &self.shared_pagination_status
162 }
163
164 async fn load_more_events_backwards(&self) -> Result<LoadMoreEventsBackwardsOutcome> {
165 let mut state = self.state.write().await?;
166
167 if let Some(prev_token) = state.room_linked_chunk().rgap().map(|gap| gap.token) {
170 return Ok(LoadMoreEventsBackwardsOutcome::Gap {
171 prev_token: Some(prev_token),
172 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
173 });
174 }
175
176 let prev_first_chunk = state.room_linked_chunk().first_chunk();
177
178 let linked_chunk_id = LinkedChunkId::Room(&state.state.room_id);
180 let new_first_chunk = match state
181 .store
182 .load_previous_chunk(linked_chunk_id, prev_first_chunk.identifier())
183 .await
184 {
185 Ok(Some(new_first_chunk)) => {
186 new_first_chunk
188 }
189
190 Ok(None) => {
191 if state.room_linked_chunk().events().next().is_some() {
197 trace!("chunk is fully loaded and non-empty: reached_start=true");
201 return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
202 }
203
204 return Ok(LoadMoreEventsBackwardsOutcome::Gap {
206 prev_token: None,
207 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
208 });
209 }
210
211 Err(err) => {
212 error!("error when loading the previous chunk of a linked chunk: {err}");
213
214 state
216 .store
217 .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
218 .await?;
219
220 return Err(err.into());
222 }
223 };
224
225 let chunk_content = new_first_chunk.content.clone();
226
227 let reached_start = new_first_chunk.previous.is_none();
233
234 if let Err(err) = state.room_linked_chunk_mut().insert_new_chunk_as_first(new_first_chunk) {
235 error!("error when inserting the previous chunk into its linked chunk: {err}");
236
237 state
239 .store
240 .handle_linked_chunk_updates(
241 LinkedChunkId::Room(&state.state.room_id),
242 vec![Update::Clear],
243 )
244 .await?;
245
246 return Err(err.into());
248 }
249
250 let _ = state.room_linked_chunk_mut().store_updates().take();
253
254 let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
256
257 Ok(match chunk_content {
258 ChunkContent::Gap(gap) => {
259 trace!("reloaded chunk from disk (gap)");
260
261 LoadMoreEventsBackwardsOutcome::Gap {
262 prev_token: Some(gap.token),
263 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
264 }
265 }
266
267 ChunkContent::Items(events) => {
268 trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());
269
270 let reveals_a_receipt_target = self.weak_room.get().is_some_and(|room| {
276 let read_receipts = room.read_receipts();
277
278 contains_a_receipt_target(&events, &unresolved_receipt_targets(&read_receipts))
279 });
280
281 if reveals_a_receipt_target {
282 let result = state.update_read_receipts(None).await;
283
284 if let Err(err) = result {
285 error!(
286 ?err,
287 "error when recomputing the read receipts after loading a chunk from disk"
288 );
289 }
290 }
291
292 LoadMoreEventsBackwardsOutcome::Events {
293 events,
294 timeline_event_diffs,
295 reached_start,
296 }
297 }
298 })
299 }
300
301 async fn mark_has_waited_for_initial_prev_token(&self) -> Result<()> {
302 *self.state.write().await?.waited_for_initial_prev_token_mut() = true;
303
304 Ok(())
305 }
306
307 async fn wait_for_prev_token(&self) {
308 self.pagination_batch_token_notifier.notified().await
309 }
310
311 async fn paginate_backwards_with_network(
312 &self,
313 batch_size: u16,
314 prev_token: &Option<String>,
315 ) -> Result<Option<(Vec<Event>, Option<String>)>> {
316 let Some(room) = self.weak_room.get() else {
317 return Ok(None);
319 };
320
321 let mut options = MessagesOptions::new(Direction::Backward).from(prev_token.as_deref());
322 options.limit = batch_size.into();
323
324 let response = room
325 .messages(options)
326 .await
327 .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
328
329 Ok(Some((response.chunk, response.end)))
330 }
331
332 async fn conclude_backwards_pagination_from_disk(
333 &self,
334 events: Vec<Event>,
335 timeline_event_diffs: Vec<VectorDiff<Event>>,
336 reached_start: bool,
337 ) -> BackPaginationOutcome {
338 if !timeline_event_diffs.is_empty() {
339 self.update_sender.send(
340 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
341 diffs: timeline_event_diffs,
342 origin: EventsOrigin::Cache,
343 }),
344 Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
345 );
346 }
347
348 BackPaginationOutcome {
349 reached_start,
350 events: events.into_iter().rev().collect(),
353 }
354 }
355
356 async fn conclude_backwards_pagination_from_network(
357 &self,
358 events: Vec<Event>,
359 prev_token: Option<String>,
360 mut new_token: Option<String>,
361 ) -> Result<Option<BackPaginationOutcome>> {
362 let mut state = self.state.write().await?;
363
364 let prev_gap_id = if let Some(token) = prev_token {
367 let gap_chunk_id = state.room_linked_chunk().chunk_identifier(|chunk| {
369 matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)
370 });
371
372 if gap_chunk_id.is_none() {
373 return Ok(None);
380 }
381
382 gap_chunk_id
383 } else {
384 None
385 };
386
387 let DeduplicationOutcome {
388 all_events: mut events,
389 in_memory_duplicated_event_ids,
390 in_store_duplicated_event_ids,
391 non_empty_all_duplicates: all_duplicates,
392 } = filter_duplicate_events(
393 &state.state.own_user_id,
394 &state.store,
395 LinkedChunkId::Room(&state.state.room_id),
396 state.room_linked_chunk(),
397 events,
398 )
399 .await?;
400
401 if !all_duplicates {
416 state
418 .remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids)
419 .await?;
420 } else {
421 events.clear();
423 new_token = None;
426 }
427
428 let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();
431
432 let new_gap = new_token.as_ref().map(|prev_token| Gap { token: prev_token.clone() });
433 let reached_start = state.room_linked_chunk_mut().push_backwards_pagination_events(
434 prev_gap_id,
435 new_gap,
436 &topo_ordered_events,
437 );
438
439 state.propagate_changes().await?;
441
442 let receipt_event = None;
450
451 state.post_process_upserted_events(topo_ordered_events.iter(), receipt_event).await?;
453
454 let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
455
456 if !timeline_event_diffs.is_empty() {
457 self.update_sender.send(
458 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
459 diffs: timeline_event_diffs,
460 origin: EventsOrigin::Pagination,
461 }),
462 Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
463 );
464 }
465
466 Ok(Some(BackPaginationOutcome { events, reached_start }))
467 }
468}
469
470impl fmt::Debug for RoomPagination {
471 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
472 formatter.debug_tuple("RoomPagination").finish_non_exhaustive()
473 }
474}