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