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 futures_core::{Stream, ready};
29use matrix_sdk_base::{
30 event_cache::{Event, Gap},
31 linked_chunk::{ChunkContent, LinkedChunkId, Update},
32};
33use pin_project_lite::pin_project;
34use ruma::api::Direction;
35use tracing::{error, trace};
36
37pub use super::super::pagination::PaginationStatus;
38use super::{
39 super::{
40 super::{
41 EventCacheError, EventsOrigin, Result, RoomEventCacheGenericUpdate,
42 deduplicator::{DeduplicationOutcome, filter_duplicate_events},
43 },
44 TimelineVectorDiffs,
45 pagination::{
46 BackPaginationOutcome, LoadMoreEventsBackwardsOutcome, PaginatedCache, Pagination,
47 },
48 read_receipts::{contains_a_receipt_target, unresolved_receipt_targets},
49 },
50 RoomEventCacheInner, RoomEventCacheUpdate,
51};
52use crate::{event_cache::caches::pagination::SharedPaginationStatus, room::MessagesOptions};
53
54pin_project! {
55 pub struct PaginationStatusSubscriber {
60 #[pin]
61 subscriber: Subscriber<SharedPaginationStatus>,
62 }
63}
64
65#[cfg(not(tarpaulin_include))]
66impl fmt::Debug for PaginationStatusSubscriber {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.debug_struct("PaginationStatusSubscriber").finish_non_exhaustive()
69 }
70}
71
72impl PaginationStatusSubscriber {
73 fn map(from: SharedPaginationStatus) -> PaginationStatus {
74 match from {
75 SharedPaginationStatus::Idle { hit_timeline_start } => {
76 PaginationStatus::Idle { hit_timeline_start }
77 }
78 SharedPaginationStatus::Paginating { .. } => PaginationStatus::Paginating,
79 }
80 }
81
82 pub fn get(&self) -> PaginationStatus {
83 Self::map(self.subscriber.get())
84 }
85
86 pub async fn next(&mut self) -> Option<PaginationStatus> {
87 self.subscriber.next().await.map(Self::map)
88 }
89
90 pub fn next_now(&mut self) -> PaginationStatus {
91 Self::map(self.subscriber.next_now())
92 }
93}
94
95impl Stream for PaginationStatusSubscriber {
96 type Item = PaginationStatus;
97
98 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
99 Poll::Ready(ready!(self.project().subscriber.as_mut().poll_next(cx)).map(Self::map))
100 }
101}
102
103#[allow(missing_debug_implementations)]
110#[derive(Clone)]
111pub struct RoomPagination(Pagination<Arc<RoomEventCacheInner>>);
112
113impl RoomPagination {
114 pub(super) fn new(cache: Arc<RoomEventCacheInner>) -> Self {
116 Self(Pagination::new(cache))
117 }
118
119 pub async fn run_backwards_until(
131 &self,
132 num_requested_events: u16,
133 ) -> Result<BackPaginationOutcome> {
134 self.0.run_backwards_until(num_requested_events).await
135 }
136
137 pub async fn run_backwards_once(&self, batch_size: u16) -> Result<BackPaginationOutcome> {
142 self.0.run_backwards_once(batch_size).await
143 }
144
145 pub fn status(&self) -> PaginationStatusSubscriber {
147 PaginationStatusSubscriber { subscriber: self.0.cache.status().subscribe() }
148 }
149
150 #[cfg(test)]
151 pub(super) async fn load_more_events_backwards(
152 &self,
153 ) -> Result<LoadMoreEventsBackwardsOutcome> {
154 self.0.cache.load_more_events_backwards().await
155 }
156}
157
158impl PaginatedCache for Arc<RoomEventCacheInner> {
159 fn status(&self) -> &SharedObservable<SharedPaginationStatus> {
160 &self.shared_pagination_status
161 }
162
163 async fn load_more_events_backwards(&self) -> Result<LoadMoreEventsBackwardsOutcome> {
164 let mut state = self.state.write().await?;
165
166 if let Some(prev_token) = state.room_linked_chunk().rgap().map(|gap| gap.token) {
169 return Ok(LoadMoreEventsBackwardsOutcome::Gap {
170 prev_token: Some(prev_token),
171 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
172 });
173 }
174
175 let prev_first_chunk = state.room_linked_chunk().first_chunk();
176
177 let linked_chunk_id = LinkedChunkId::Room(&state.state.room_id);
179 let new_first_chunk = match state
180 .store
181 .load_previous_chunk(linked_chunk_id, prev_first_chunk.identifier())
182 .await
183 {
184 Ok(Some(new_first_chunk)) => {
185 new_first_chunk
187 }
188
189 Ok(None) => {
190 if state.room_linked_chunk().events().next().is_some() {
196 trace!("chunk is fully loaded and non-empty: reached_start=true");
200 return Ok(LoadMoreEventsBackwardsOutcome::StartOfTimeline);
201 }
202
203 return Ok(LoadMoreEventsBackwardsOutcome::Gap {
205 prev_token: None,
206 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
207 });
208 }
209
210 Err(err) => {
211 error!("error when loading the previous chunk of a linked chunk: {err}");
212
213 state
215 .store
216 .handle_linked_chunk_updates(linked_chunk_id, vec![Update::Clear])
217 .await?;
218
219 return Err(err.into());
221 }
222 };
223
224 let chunk_content = new_first_chunk.content.clone();
225
226 let reached_start = new_first_chunk.previous.is_none();
232
233 if let Err(err) = state.room_linked_chunk_mut().insert_new_chunk_as_first(new_first_chunk) {
234 error!("error when inserting the previous chunk into its linked chunk: {err}");
235
236 state
238 .store
239 .handle_linked_chunk_updates(
240 LinkedChunkId::Room(&state.state.room_id),
241 vec![Update::Clear],
242 )
243 .await?;
244
245 return Err(err.into());
247 }
248
249 let _ = state.room_linked_chunk_mut().store_updates().take();
252
253 let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
255
256 Ok(match chunk_content {
257 ChunkContent::Gap(gap) => {
258 trace!("reloaded chunk from disk (gap)");
259
260 LoadMoreEventsBackwardsOutcome::Gap {
261 prev_token: Some(gap.token),
262 waited_for_initial_prev_token: state.waited_for_initial_prev_token(),
263 }
264 }
265
266 ChunkContent::Items(events) => {
267 trace!(?reached_start, "reloaded chunk from disk ({} items)", events.len());
268
269 let reveals_a_receipt_target = self.weak_room.get().is_some_and(|room| {
275 let read_receipts = room.read_receipts();
276
277 contains_a_receipt_target(&events, &unresolved_receipt_targets(&read_receipts))
278 });
279
280 if reveals_a_receipt_target {
281 let result = state.update_read_receipts(None).await;
282
283 if let Err(err) = result {
284 error!(
285 ?err,
286 "error when recomputing the read receipts after loading a chunk from disk"
287 );
288 }
289 }
290
291 if !timeline_event_diffs.is_empty() {
295 state.update_sender.send(
296 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
297 diffs: timeline_event_diffs,
298 origin: EventsOrigin::Cache,
299 }),
300 Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
301 );
302 }
303
304 LoadMoreEventsBackwardsOutcome::Events { events, reached_start }
305 }
306 })
307 }
308
309 async fn mark_has_waited_for_initial_prev_token(&self) -> Result<()> {
310 *self.state.write().await?.waited_for_initial_prev_token_mut() = true;
311
312 Ok(())
313 }
314
315 async fn wait_for_prev_token(&self) {
316 self.pagination_batch_token_notifier.notified().await
317 }
318
319 async fn paginate_backwards_with_network(
320 &self,
321 batch_size: u16,
322 prev_token: &Option<String>,
323 ) -> Result<Option<(Vec<Event>, Option<String>)>> {
324 let Some(room) = self.weak_room.get() else {
325 return Ok(None);
327 };
328
329 let mut options = MessagesOptions::new(Direction::Backward).from(prev_token.as_deref());
330 options.limit = batch_size.into();
331
332 let response = room
333 .messages(options)
334 .await
335 .map_err(|err| EventCacheError::PaginationError(Arc::new(err)))?;
336
337 Ok(Some((response.chunk, response.end)))
338 }
339
340 async fn conclude_backwards_pagination_from_network(
341 &self,
342 events: Vec<Event>,
343 prev_token: Option<String>,
344 mut new_token: Option<String>,
345 ) -> Result<Option<BackPaginationOutcome>> {
346 let mut state = self.state.write().await?;
347
348 let prev_gap_id = if let Some(token) = prev_token {
351 let gap_chunk_id = state.room_linked_chunk().chunk_identifier(|chunk| {
353 matches!(chunk.content(), ChunkContent::Gap(Gap { token: prev_token }) if *prev_token == token)
354 });
355
356 if gap_chunk_id.is_none() {
357 return Ok(None);
364 }
365
366 gap_chunk_id
367 } else {
368 None
369 };
370
371 let DeduplicationOutcome {
372 all_events: mut events,
373 in_memory_duplicated_event_ids,
374 in_store_duplicated_event_ids,
375 non_empty_all_duplicates: all_duplicates,
376 } = filter_duplicate_events(
377 &state.state.own_user_id,
378 &state.store,
379 LinkedChunkId::Room(&state.state.room_id),
380 state.room_linked_chunk(),
381 events,
382 )
383 .await?;
384
385 if !all_duplicates {
400 state
402 .remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids)
403 .await?;
404 } else {
405 events.clear();
407 new_token = None;
410 }
411
412 let topo_ordered_events = events.iter().rev().cloned().collect::<Vec<_>>();
415
416 let new_gap = new_token.as_ref().map(|prev_token| Gap { token: prev_token.clone() });
417 let reached_start = state.room_linked_chunk_mut().push_backwards_pagination_events(
418 prev_gap_id,
419 new_gap,
420 &topo_ordered_events,
421 );
422
423 state.propagate_changes().await?;
425
426 let receipt_event = None;
434
435 state.post_process_upserted_events(topo_ordered_events.iter(), receipt_event).await?;
437
438 let timeline_event_diffs = state.room_linked_chunk_mut().updates_as_vector_diffs();
439
440 if !timeline_event_diffs.is_empty() {
441 self.update_sender.send(
442 RoomEventCacheUpdate::UpdateTimelineEvents(TimelineVectorDiffs {
443 diffs: timeline_event_diffs,
444 origin: EventsOrigin::Pagination,
445 }),
446 Some(RoomEventCacheGenericUpdate { room_id: self.room_id.clone() }),
447 );
448 }
449
450 Ok(Some(BackPaginationOutcome { events, reached_start }))
451 }
452}
453
454impl fmt::Debug for RoomPagination {
455 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
456 formatter.debug_tuple("RoomPagination").finish_non_exhaustive()
457 }
458}