Skip to main content

matrix_sdk/event_cache/caches/pinned_events/
mod.rs

1// Copyright 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15mod updates;
16
17use std::{cmp::Ordering, collections::BTreeSet, fmt, sync::Arc};
18
19use eyeball_im::VectorDiff;
20use futures_util::{StreamExt as _, stream};
21use matrix_sdk_base::{
22    apply_redaction,
23    event_cache::{Event, Gap},
24    linked_chunk::{LinkedChunkId, OwnedLinkedChunkId, Position, Update},
25    serde_helpers::{extract_redaction_target, extract_relation},
26    sync::Timeline,
27    task_monitor::BackgroundTaskHandle,
28};
29use matrix_sdk_common::executor::spawn;
30use ruma::{
31    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId,
32    events::{relation::RelationType, room::redaction::SyncRoomRedactionEvent},
33    room_version_rules::RoomVersionRules,
34};
35use tokio::sync::broadcast::Receiver;
36use tracing::{debug, instrument, trace, warn};
37
38pub(super) use self::updates::PinnedEventsCacheUpdateSender;
39#[cfg(feature = "e2e-encryption")]
40use super::super::redecryptor::MaybeResolvedEvent;
41use super::{
42    super::{
43        EventCacheError, EventsOrigin, Result,
44        deduplicator::{DeduplicationOutcome, filter_duplicate_events},
45        persistence::{find_event, send_updates_to_store},
46        states::{
47            CacheStateLock, ReloadPreprocessing, StateLock, StateLockWriteGuard,
48            selectors::PinnedEventsStateSelector,
49        },
50    },
51    EventLocation, TimelineVectorDiffs,
52    event_linked_chunk::{EventLinkedChunk, sort_positions_descending},
53};
54use crate::{
55    Room,
56    config::RequestConfig,
57    event_cache::{caches::room::LinkedChunkUpdateSender, deduplicator::is_valid_event},
58    room::WeakRoom,
59};
60
61pub struct PinnedEventsCacheState {
62    /// The ID of the room owning this list of pinned events.
63    room_id: OwnedRoomId,
64
65    /// The user's own user id.
66    own_user_id: OwnedUserId,
67
68    /// The rules for the version of this room.
69    room_version_rules: RoomVersionRules,
70
71    /// The linked chunk representing this room's pinned events.
72    ///
73    /// This linked chunk also contains related events. The events are sorted in
74    /// the chronological order (oldest to newest), since it would be otherwise
75    /// impossible to order them correctly, given that we fetch their relations
76    /// over time.
77    chunk: EventLinkedChunk,
78
79    /// Update sender for this pinned events cache.
80    pub update_sender: PinnedEventsCacheUpdateSender,
81
82    /// A sender for the globally observable linked chunk updates that happened
83    /// during a sync or a back-pagination.
84    ///
85    /// See also [`super::super::EventCacheInner::linked_chunk_update_sender`].
86    linked_chunk_update_sender: LinkedChunkUpdateSender,
87}
88
89#[cfg(not(tarpaulin_include))]
90impl fmt::Debug for PinnedEventsCacheState {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.debug_struct("PinnedEventsCacheState")
93            .field("room_id", &self.room_id)
94            .field("chunk", &self.chunk)
95            .finish_non_exhaustive()
96    }
97}
98
99impl<'a> StateLockWriteGuard<'a, PinnedEventsCacheState> {
100    /// Reload the pinned-events: only the last events will be reloaded,
101    /// shrinking the in-memory size of the cache.
102    ///
103    /// If `preprocessing` is set to [`ReloadPreprocessing::ForgetAll`], all
104    /// events will be erased before reloaded.
105    #[must_use = "Propagate `VectorDiff` updates via `TimelineVectorDiffs`"]
106    pub async fn reload(
107        &mut self,
108        preprocessing: ReloadPreprocessing,
109    ) -> Result<Vec<VectorDiff<Event>>> {
110        match preprocessing {
111            ReloadPreprocessing::ForgetAll => {
112                // Clear the `LinkedChunk` and broadcast the updates to the
113                // store.
114                self.state.chunk.reset();
115                self.propagate_changes().await?;
116            }
117
118            ReloadPreprocessing::None => {}
119        }
120
121        // The task will notice there is a desynchronisation and will reload
122        // from network.
123        self.reload_from_storage().await?;
124
125        Ok(self.state.chunk.updates_as_vector_diffs())
126    }
127
128    async fn handle_sync(&mut self, timeline: Timeline) -> Result<()> {
129        let DeduplicationOutcome {
130            all_events: events,
131            in_memory_duplicated_event_ids,
132            in_store_duplicated_event_ids,
133            non_empty_all_duplicates: all_duplicates,
134        } = filter_duplicate_events(
135            &self.state.own_user_id,
136            &self.store,
137            LinkedChunkId::PinnedEvents(&self.state.room_id),
138            &self.state.chunk,
139            timeline.events,
140        )
141        .await?;
142
143        if all_duplicates {
144            // If all events are duplicates, we don't need to do anything;
145            // ignore the new events.
146            return Ok(());
147        }
148
149        // Remove the old duplicated events.
150        //
151        // We don't have to worry about the removals can change the position of
152        // the existing events, because we are pushing all _new_ `events` at the
153        // back.
154        self.remove_events(in_memory_duplicated_event_ids, in_store_duplicated_event_ids).await?;
155
156        // We've found new relations; append them to the linked chunk.
157        self.state.chunk.push_live_events(None, &events);
158
159        self.propagate_changes().await?;
160        self.notify_subscribers(EventsOrigin::Sync);
161
162        // Do stuff for each event.
163        for event in &events {
164            // Handle redaction.
165            self.maybe_apply_new_redaction(event).await?;
166        }
167
168        Ok(())
169    }
170
171    /// Remove events by their position, in `EventLinkedChunk`.
172    ///
173    /// This method is purposely isolated because it must ensure that positions
174    /// are sorted appropriately or it can be disastrous.
175    #[instrument(skip_all)]
176    pub async fn remove_events(
177        &mut self,
178        in_memory_events: Vec<(OwnedEventId, Position)>,
179        in_store_events: Vec<(OwnedEventId, Position)>,
180    ) -> Result<()> {
181        // In-store events.
182        if !in_store_events.is_empty() {
183            let mut positions = in_store_events
184                .into_iter()
185                .map(|(_event_id, position)| position)
186                .collect::<Vec<_>>();
187
188            sort_positions_descending(&mut positions);
189
190            let updates =
191                positions.into_iter().map(|pos| Update::RemoveItem { at: pos }).collect::<Vec<_>>();
192
193            self.apply_store_only_updates(updates).await?;
194        }
195
196        // In-memory events.
197        if in_memory_events.is_empty() {
198            // Nothing else to do, return early.
199            return Ok(());
200        }
201
202        // `remove_events_by_position` is responsible of sorting positions.
203        self.state
204            .chunk
205            .remove_events_by_position(
206                in_memory_events.into_iter().map(|(_event_id, position)| position).collect(),
207            )
208            .expect("failed to remove an event");
209
210        self.propagate_changes().await
211    }
212
213    /// Apply some updates that are effective only on the store itself.
214    ///
215    /// This method should be used only for updates that happen _outside_ the
216    /// in-memory linked chunk. Such updates must be applied onto the persistent
217    /// storage.
218    async fn apply_store_only_updates(&mut self, updates: Vec<Update<Event, Gap>>) -> Result<()> {
219        self.send_updates_to_store(updates).await
220    }
221
222    /// If the given event is a redaction, try to retrieve the to-be-redacted
223    /// event in the chunk, and replace it by the redacted form.
224    #[instrument(skip_all)]
225    async fn maybe_apply_new_redaction(&mut self, event: &Event) -> Result<()> {
226        let Some(event_id) =
227            extract_redaction_target(event.raw(), &self.room_version_rules.redaction)
228        else {
229            return Ok(());
230        };
231
232        // Replace the redacted event by a redacted form, if we knew about it.
233        let Some((location, mut target_event)) = self.find_event(&event_id).await? else {
234            trace!("redacted event is missing from the linked chunk");
235            return Ok(());
236        };
237
238        let target_event_raw = target_event.raw();
239
240        // Don't redact already redacted events.
241        if let Ok(deserialized) = target_event_raw.deserialize()
242            && deserialized.is_redacted()
243        {
244            return Ok(());
245        }
246
247        if let Some(redacted_event) = apply_redaction(
248            target_event_raw,
249            event.raw().cast_ref_unchecked::<SyncRoomRedactionEvent>(),
250            &self.room_version_rules.redaction,
251        ) {
252            // It's safe to cast `redacted_event` here:
253            //
254            // - either the event was an `AnyTimelineEvent` cast to
255            //   `AnySyncTimelineEvent` when calling .raw(), so it's still one
256            //   under the hood.
257            // - or it wasn't, and it's a plain `AnySyncTimelineEvent` in this
258            //   case.
259            target_event.replace_raw(redacted_event.cast_unchecked());
260
261            self.replace_event_at(location, target_event.clone()).await?;
262        }
263
264        Ok(())
265    }
266
267    /// See documentation of [`find_event`].
268    pub(super) async fn find_event(
269        &self,
270        event_id: &EventId,
271    ) -> Result<Option<(EventLocation, Event)>> {
272        find_event(event_id, &self.room_id, &self.chunk, &self.store).await
273    }
274
275    /// Replaces a single event, be it saved in memory or in the store.
276    ///
277    /// If it was saved in memory, this will emit a notification to observers
278    /// that a single item has been replaced. Otherwise, such a notification is
279    /// not emitted, because observers are unlikely to observe the store updates
280    /// directly.
281    pub async fn replace_event_at(
282        &mut self,
283        location: EventLocation,
284        new_event: Event,
285    ) -> Result<()> {
286        match location {
287            EventLocation::Memory(position) => {
288                self.state
289                    .chunk
290                    .replace_event_at(position, new_event)
291                    .expect("should have been a valid position of an item");
292                // We just changed the in-memory representation; synchronize
293                // this with the store.
294                self.propagate_changes().await?;
295            }
296            EventLocation::Store => {
297                self.save_events([new_event]).await?;
298            }
299        }
300
301        Ok(())
302    }
303
304    /// Save events into the database, without notifying observers.
305    pub async fn save_events(&mut self, events: impl IntoIterator<Item = Event>) -> Result<()> {
306        let store = self.store.clone();
307        let room_id = self.state.room_id.clone();
308        let events = events.into_iter().collect::<Vec<_>>();
309
310        // Spawn a task so the save is uninterrupted by task cancellation.
311        spawn(async move {
312            for event in events {
313                store.save_event(&room_id, event).await?;
314            }
315
316            Result::Ok(())
317        })
318        .await
319        .expect("joining failed")?;
320
321        Ok(())
322    }
323
324    /// Reload all the pinned events from storage, replacing the current linked
325    /// chunk.
326    async fn reload_from_storage(&mut self) -> Result<()> {
327        let room_id = self.state.room_id.clone();
328        let linked_chunk_id = LinkedChunkId::PinnedEvents(&room_id);
329
330        let (last_chunk, chunk_id_gen) = self.store.load_last_chunk(linked_chunk_id).await?;
331
332        let Some(last_chunk) = last_chunk else {
333            // No pinned events stored, make sure the in-memory linked chunk is
334            // sync'd (i.e. empty), and return.
335            if self.state.chunk.events().next().is_some() {
336                self.state.chunk.reset();
337                self.notify_subscribers(EventsOrigin::Sync);
338            }
339
340            return Ok(());
341        };
342
343        {
344            let mut current_chunk_identifier = last_chunk.identifier;
345            self.state.chunk.shrink_to_last_reloaded_chunk(
346                Some(last_chunk),
347                chunk_id_gen,
348                // This cache doesn't use the `OrderTracker`.
349                None,
350            )?;
351
352            // Reload the entire chunk.
353            while let Some(previous_chunk) =
354                self.store.load_previous_chunk(linked_chunk_id, current_chunk_identifier).await?
355            {
356                current_chunk_identifier = previous_chunk.identifier;
357                self.state.chunk.insert_new_chunk_as_first(previous_chunk)?;
358            }
359        }
360
361        // Empty store updates, since we just reloaded from storage.
362        self.state.chunk.store_updates().take();
363
364        // Let observers know about it.
365        self.notify_subscribers(EventsOrigin::Cache);
366
367        Ok(())
368    }
369
370    async fn replace_all_events(&mut self, mut new_events: Vec<Event>) -> Result<()> {
371        trace!("resetting all pinned events in linked chunk");
372
373        // Invalid events - those with no ID or no event type - are never
374        // persisted by the stores, so they must not reach the in-memory
375        // linked chunk either: otherwise the positions in memory and in
376        // the store would diverge.
377        new_events.retain(is_valid_event);
378
379        let previous_pinned_event_ids = self.state.current_event_ids();
380
381        if new_events
382            .iter()
383            .filter_map(|e| e.event_id())
384            .map(ToOwned::to_owned)
385            .collect::<BTreeSet<_>>()
386            == previous_pinned_event_ids.into_iter().collect()
387        {
388            // No change in the list of pinned events.
389            return Ok(());
390        }
391
392        if self.state.chunk.events().next().is_some() {
393            self.state.chunk.reset();
394        }
395
396        self.state.chunk.push_live_events(None, &new_events);
397        self.propagate_changes().await?;
398        self.notify_subscribers(EventsOrigin::Sync);
399
400        Ok(())
401    }
402
403    /// Propagate the changes in this linked chunk to observers, and save the
404    /// changes on disk.
405    pub async fn propagate_changes(&mut self) -> Result<()> {
406        let updates = self.state.chunk.store_updates().take();
407
408        self.send_updates_to_store(updates).await
409    }
410
411    async fn send_updates_to_store(&mut self, updates: Vec<Update<Event, Gap>>) -> Result<()> {
412        let linked_chunk_id = OwnedLinkedChunkId::PinnedEvents(self.room_id.clone());
413
414        send_updates_to_store(
415            &self.store,
416            linked_chunk_id,
417            &self.state.linked_chunk_update_sender,
418            updates,
419        )
420        .await
421    }
422
423    /// Notify subscribers of timeline updates.
424    fn notify_subscribers(&mut self, origin: EventsOrigin) {
425        let diffs = self.state.chunk.updates_as_vector_diffs();
426
427        if !diffs.is_empty() {
428            self.update_sender.send(TimelineVectorDiffs { diffs, origin });
429        }
430    }
431}
432
433impl PinnedEventsCacheState {
434    /// Return a list of the current event IDs in this linked chunk.
435    pub(super) fn current_event_ids(&self) -> Vec<OwnedEventId> {
436        self.chunk
437            .events()
438            .filter_map(|(_position, event)| event.event_id().map(ToOwned::to_owned))
439            .collect()
440    }
441
442    /// Returns whether this contains exactly the given pinned events,
443    /// along with the events related to them (reactions, edits, redactions,
444    /// etc).
445    ///
446    /// Related events are ignored here when comparing the pinned event IDs
447    /// to all of the [`Self::current_event_ids`], as that would cause
448    /// differences if one pinned event has a related event, and then reload
449    /// them all over again.
450    fn has_exactly_pinned_events(&self, pinned_event_ids: &[OwnedEventId]) -> bool {
451        let pinned_event_ids: BTreeSet<&EventId> =
452            pinned_event_ids.iter().map(|event_id| &**event_id).collect();
453        let event_ids: BTreeSet<&EventId> =
454            self.chunk.events().filter_map(|(_position, event)| event.event_id()).collect();
455
456        if !pinned_event_ids.is_subset(&event_ids) {
457            return false;
458        }
459
460        // Every other event must relate to an event in this linked chunk,
461        // just like `aggregate_timeline_for_pinned_events` does for a sync.
462        // If not, then that event isn't pinned anymore.
463        self.chunk.events().all(|(_position, event)| {
464            event.event_id().is_some_and(|event_id| pinned_event_ids.contains(event_id))
465                || extract_relation(event.raw()).is_some_and(|(relation_type, related_event_id)| {
466                    relation_type != RelationType::Thread && event_ids.contains(&*related_event_id)
467                })
468                || extract_redaction_target(event.raw(), &self.room_version_rules.redaction)
469                    .is_some_and(|redacted_event_id| event_ids.contains(&*redacted_event_id))
470        })
471    }
472}
473
474/// All the information related to room's pinned events..
475///
476/// Cloning is shallow, and thus is cheap to do.
477#[derive(Clone)]
478pub struct PinnedEventsCache {
479    inner: Arc<PinnedEventsCacheInner>,
480
481    /// The task handling the refreshing of pinned events for this specific
482    /// room.
483    _task: Arc<BackgroundTaskHandle>,
484}
485
486/// The (non-cloneable) details of the `PinnedEventsCache`.
487struct PinnedEventsCacheInner {
488    /// The ID of the room owning this list of pinned events.
489    room_id: OwnedRoomId,
490
491    /// State of this `PinnedEventsCache`.
492    ///
493    /// It is behind an `Arc` because it is shared with the task.
494    state: CacheStateLock<PinnedEventsStateSelector>,
495}
496
497impl PinnedEventsCache {
498    /// Creates a new [`PinnedEventsCache`] for the given room.
499    pub(in super::super) async fn new(
500        weak_room: &WeakRoom,
501        own_user_id: OwnedUserId,
502        room_version_rules: RoomVersionRules,
503        linked_chunk_update_sender: LinkedChunkUpdateSender,
504        state: &StateLock,
505    ) -> Result<Self> {
506        let room = weak_room.get().ok_or(EventCacheError::ClientDropped)?;
507        let room_id = room.room_id().to_owned();
508
509        let cache_state = state
510            .try_insert_once_with(
511                PinnedEventsStateSelector::new(room_id.clone()),
512                |_store_guard| async {
513                    Ok(PinnedEventsCacheState {
514                        room_id: room_id.clone(),
515                        own_user_id,
516                        room_version_rules,
517                        chunk: EventLinkedChunk::new(),
518                        update_sender: PinnedEventsCacheUpdateSender::new(),
519                        linked_chunk_update_sender,
520                    })
521                },
522            )
523            .await?;
524
525        let inner = Arc::new(PinnedEventsCacheInner { room_id, state: cache_state });
526
527        let task = room
528            .client()
529            .task_monitor()
530            .spawn_infinite_task(
531                "pinned_event_listener_task",
532                Self::pinned_event_listener_task(room, inner.clone()),
533            )
534            .abort_on_drop();
535
536        Ok(Self { inner, _task: Arc::new(task) })
537    }
538
539    /// Get the room ID of this cache.
540    pub fn room_id(&self) -> &RoomId {
541        &self.inner.room_id
542    }
543
544    /// Return a reference to the state.
545    pub(super) fn state(&self) -> &CacheStateLock<PinnedEventsStateSelector> {
546        &self.inner.state
547    }
548
549    /// Subscribe to live events from this room's pinned events cache.
550    pub async fn subscribe(&self) -> Result<(Vec<Event>, Receiver<TimelineVectorDiffs>)> {
551        let guard = self.inner.state.read().await?;
552        let events = guard.state.chunk.events().map(|(_position, item)| item.clone()).collect();
553
554        let recv = guard.state.update_sender.new_pinned_events_receiver();
555
556        Ok((events, recv))
557    }
558
559    /// Try to locate the events in the linked chunk corresponding to the given
560    /// list of resolved events, and replace them, while alerting observers
561    /// about the update.
562    #[cfg(feature = "e2e-encryption")]
563    pub(in super::super) async fn replace_in_memory_utds(
564        &self,
565        resolved_events: &[MaybeResolvedEvent],
566    ) -> Result<()> {
567        let mut state = self.inner.state.write().await?;
568
569        // Drain the updates to the store, events have already been updated
570        // before calling this method.
571        let _ = state.state.chunk.store_updates().take();
572
573        if state.state.chunk.replace_utds(resolved_events) {
574            state.propagate_changes().await?;
575            state.notify_subscribers(EventsOrigin::Cache);
576        }
577
578        Ok(())
579    }
580
581    /// Handle an update from a joined room.
582    #[instrument(skip_all, fields(room_id = %self.inner.room_id))]
583    pub(super) async fn handle_joined_room_update(&self, timeline: Timeline) -> Result<()> {
584        self.handle_timeline(timeline).await
585    }
586
587    /// Handle an update from a left room.
588    #[instrument(skip_all, fields(room_id = %self.inner.room_id))]
589    pub(super) async fn handle_left_room_update(&self, timeline: Timeline) -> Result<()> {
590        self.handle_timeline(timeline).await
591    }
592
593    /// Handle a [`Timeline`], i.e. new events received by a sync for this
594    /// thread.
595    async fn handle_timeline(&self, timeline: Timeline) -> Result<()> {
596        if timeline.events.is_empty() {
597            return Ok(());
598        }
599
600        trace!("adding new {} events", timeline.events.len());
601
602        self.inner.state.write().await?.handle_sync(timeline).await
603    }
604
605    #[instrument(fields(%room_id = room.room_id()), skip(room, inner))]
606    async fn pinned_event_listener_task(room: Room, inner: Arc<PinnedEventsCacheInner>) {
607        debug!("pinned events listener task started");
608
609        let max_pinned_events_to_load = room
610            .client()
611            .event_cache()
612            .expect("Pinned-events are running inside the Event Cache")
613            .config()
614            .max_pinned_events_to_load;
615
616        let reload_from_network = async |room: Room| {
617            let events = match Self::reload_pinned_events(room).await {
618                Ok(Some(events)) => events,
619                Ok(None) => Vec::new(),
620                Err(err) => {
621                    warn!("error when loading pinned events: {err}");
622                    return;
623                }
624            };
625
626            // Replace the whole linked chunk with those new events, and
627            // propagate updates to the observers.
628            match inner.state.write().await {
629                Ok(mut guard) => {
630                    guard.replace_all_events(events).await.unwrap_or_else(|err| {
631                        warn!("error when replacing pinned events: {err}");
632                    });
633                }
634
635                Err(err) => {
636                    warn!("error when acquiring write lock to replace pinned events: {err}");
637                }
638            }
639        };
640
641        // Reload the pinned events from the storage first.
642        match inner.state.write().await {
643            Ok(mut guard) => {
644                // On startup, reload the pinned events from storage.
645                guard.reload_from_storage().await.unwrap_or_else(|err| {
646                    warn!("error when reloading pinned events from storage, at start: {err}");
647                });
648
649                // Compare the initial list of pinned events to the one in the
650                // linked chunk.
651                let actual_pinned_events = pinned_event_ids_to_load(
652                    max_pinned_events_to_load,
653                    room.pinned_event_ids().unwrap_or_default(),
654                );
655
656                if !guard.state.has_exactly_pinned_events(&actual_pinned_events) {
657                    // Reload the list of pinned events from network.
658                    drop(guard);
659                    reload_from_network(room.clone()).await;
660                }
661            }
662
663            Err(err) => {
664                warn!("error when acquiring write lock to initialize pinned events: {err}");
665            }
666        }
667
668        let weak_room = WeakRoom::new(room.client().downgrade(), room.room_id().to_owned());
669
670        let mut stream = room.pinned_event_ids_stream();
671
672        drop(room);
673
674        // Whenever the list of pinned events changes, reload it.
675        while let Some(new_list) = stream.next().await {
676            trace!("handling update");
677
678            let Some(room) = weak_room.get() else {
679                debug!("room has been dropped, ending pinned events listener task");
680                break;
681            };
682
683            let new_list = pinned_event_ids_to_load(max_pinned_events_to_load, new_list);
684
685            let guard = match inner.state.read().await {
686                Ok(guard) => guard,
687                Err(err) => {
688                    warn!("error when acquiring read lock to handle pinned events update: {err}");
689                    break;
690                }
691            };
692
693            // Compare to the current linked chunk.
694            if guard.state.has_exactly_pinned_events(&new_list) {
695                // All the events in the pinned list are the same, don't reload.
696                continue;
697            }
698
699            drop(guard);
700
701            // Event IDs differ, so reload all the pinned events.
702            reload_from_network(room).await;
703        }
704
705        debug!("pinned events listener task ended");
706    }
707
708    /// Loads the pinned events in this room, using the cache first and then
709    /// requesting the event from the homeserver if it couldn't be found. This
710    /// method will perform as many concurrent requests for events as
711    /// `max_concurrent_requests` allows, to avoid overwhelming the server.
712    ///
713    /// Returns `None` if the list of pinned events hasn't changed since the
714    /// previous time we loaded them. May return an error if there was an issue
715    /// fetching the full events.
716    async fn reload_pinned_events(room: Room) -> Result<Option<Vec<Event>>> {
717        let (max_concurrent_requests, max_pinned_events_to_load) = {
718            let client = room.client();
719            let config = client
720                .event_cache()
721                .expect("Pinned-events are running inside the Event Cache")
722                .config();
723
724            (config.max_pinned_events_concurrent_requests, config.max_pinned_events_to_load)
725        };
726
727        let pinned_event_ids = pinned_event_ids_to_load(
728            max_pinned_events_to_load,
729            room.pinned_event_ids().unwrap_or_default(),
730        );
731
732        if pinned_event_ids.is_empty() {
733            return Ok(Some(Vec::new()));
734        }
735
736        let mut num_successful_loads = 0;
737
738        let mut loaded_events: Vec<Event> =
739            stream::iter(pinned_event_ids.clone().into_iter().map(|event_id| {
740                let room = room.clone();
741                let filter = vec![RelationType::Annotation, RelationType::Replacement];
742                let request_config = RequestConfig::default().retry_limit(3);
743
744                async move {
745                    let (target, mut relations) = room
746                        .load_or_fetch_event_with_relations(
747                            &event_id,
748                            Some(filter),
749                            Some(request_config),
750                        )
751                        .await?;
752
753                    relations.insert(0, target);
754                    Ok::<_, crate::Error>(relations)
755                }
756            }))
757            .buffer_unordered(max_concurrent_requests)
758            // Count successful queries.
759            .inspect(|result| {
760                if result.is_ok() {
761                    num_successful_loads += 1;
762                }
763            })
764            // Get rid of error results.
765            .flat_map(stream::iter)
766            // Flatten the list of `Vec<Event>` into a list of `Event`.
767            .flat_map(stream::iter)
768            .collect()
769            .await;
770
771        if num_successful_loads != pinned_event_ids.len() {
772            warn!(
773                "only successfully loaded {} out of {} pinned events",
774                num_successful_loads,
775                pinned_event_ids.len()
776            );
777        }
778
779        if loaded_events.is_empty() {
780            // If the list of loaded events is empty, we ran into an error to
781            // load _all_ the pinned events, which needs to be reported to the
782            // caller.
783            return Err(EventCacheError::UnableToLoadPinnedEvents);
784        }
785
786        // Since we have all the events and their related events, we can't
787        // nicely sort them, since we've lost all ordering information from
788        // using /event or /relations. Resort to sorting using chronological
789        // ordering (oldest -> newest).
790        loaded_events.sort_by(compare_pinned_items);
791
792        Ok(Some(loaded_events))
793    }
794}
795
796impl fmt::Debug for PinnedEventsCache {
797    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
798        f.debug_struct("PinnedEventsCache").finish_non_exhaustive()
799    }
800}
801
802/// Returns the IDs of the pinned events that this cache loads
803/// from the given list of all the room's pinned events.
804///
805/// This will include only the most recently pinned ones, up to the limit of
806/// the chosen `max_pinned_events_to_load` cfg value.
807#[inline]
808fn pinned_event_ids_to_load(
809    max_events_to_load: usize,
810    pinned_event_ids: Vec<OwnedEventId>,
811) -> Vec<OwnedEventId> {
812    pinned_event_ids.into_iter().rev().take(max_events_to_load).rev().collect()
813}
814
815fn compare_pinned_items(a: &Event, b: &Event) -> Ordering {
816    let a_time: Option<MilliSecondsSinceUnixEpoch> = a.timestamp_raw();
817    let b_time: Option<MilliSecondsSinceUnixEpoch> = b.timestamp_raw();
818
819    compare_by_optional_timestamp(a_time, b_time)
820}
821
822fn compare_by_optional_timestamp(
823    a: Option<MilliSecondsSinceUnixEpoch>,
824    b: Option<MilliSecondsSinceUnixEpoch>,
825) -> Ordering {
826    match (a, b) {
827        (None, None) => Ordering::Equal,
828        (None, Some(_)) => Ordering::Greater,
829        (Some(_), None) => Ordering::Less,
830        (Some(a), Some(b)) => a.cmp(&b),
831    }
832}
833
834#[cfg(not(target_family = "wasm"))]
835#[cfg(test)]
836mod tests {
837    use proptest::prelude::*;
838    use ruma::UInt;
839
840    use super::*;
841
842    fn any_timestamp() -> impl Strategy<Value = Option<MilliSecondsSinceUnixEpoch>> {
843        prop::option::of(
844            any::<u32>().prop_map(|value| MilliSecondsSinceUnixEpoch(UInt::from(value))),
845        )
846    }
847
848    #[test]
849    fn sort_pinned_events_never_panics_only_nones() {
850        let mut vec = vec![None; 100_000];
851        vec.sort_by(|a, b| compare_by_optional_timestamp(*a, *b))
852    }
853
854    proptest! {
855    #[test]
856    fn sort_pinned_events_never_panics(mut v in prop::collection::vec(any_timestamp(), 0..1000)) {
857        v.sort_by(
858            |a, b| compare_by_optional_timestamp(*a, *b))
859    }
860
861    #[test]
862    fn compare_pinned_events_reflexive(a in any_timestamp()) {
863        prop_assert_eq!(compare_by_optional_timestamp(a, a), Ordering::Equal);
864    }
865
866    #[test]
867    fn compare_pinned_events_antisymmetric(a in any_timestamp(), b in any_timestamp()) {
868        let ab = compare_by_optional_timestamp(a, b);
869        let ba = compare_by_optional_timestamp(b, a);
870
871        prop_assert_eq!(ab, ba.reverse());
872    }
873
874    #[test]
875    fn compare_pinned_events_transitive(
876        a in any_timestamp(),
877        b in any_timestamp(),
878        c in any_timestamp()
879    ) {
880        let ab = compare_by_optional_timestamp(a, b);
881        let bc = compare_by_optional_timestamp(b, c);
882        let ac = compare_by_optional_timestamp(a, c);
883
884        if ab == Ordering::Less && bc == Ordering::Less {
885            prop_assert_eq!(ac, Ordering::Less);
886        }
887
888        if ab == Ordering::Equal && bc == Ordering::Equal {
889            prop_assert_eq!(ac, Ordering::Equal);
890        }
891
892        if ab == Ordering::Greater && bc == Ordering::Greater {
893            prop_assert_eq!(ac, Ordering::Greater);
894        }
895    }
896    }
897}