Skip to main content

matrix_sdk/event_cache/
persistence.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
15use std::collections::{HashMap, HashSet};
16
17use matrix_sdk_base::{
18    deserialized_responses::TimelineEventKind,
19    event_cache::{Event, Gap, store::EventCacheStoreLockGuard},
20    executor::spawn,
21    linked_chunk::{ChunkMetadata, LinkedChunkId, OwnedLinkedChunkId, Update},
22};
23use ruma::{EventId, RoomId, events::relation::RelationType, serde::Raw};
24use tokio::sync::broadcast::Sender;
25use tracing::trace;
26
27use super::{
28    EventCacheError, Result,
29    caches::{
30        EventLocation, event_linked_chunk::EventLinkedChunk, room::RoomEventCacheLinkedChunkUpdate,
31    },
32};
33
34/// Load a linked chunk's full metadata, making sure the chunks are
35/// correct according to their links.
36///
37/// Returns `None` if there's no such linked chunk in the store, or an
38/// error if the linked chunk is malformed.
39pub(super) async fn load_linked_chunk_metadata(
40    store_guard: &EventCacheStoreLockGuard,
41    linked_chunk_id: LinkedChunkId<'_>,
42) -> Result<Option<Vec<ChunkMetadata>>> {
43    let mut all_chunks = store_guard
44        .load_all_chunks_metadata(linked_chunk_id)
45        .await
46        .map_err(EventCacheError::from)?;
47
48    if all_chunks.is_empty() {
49        // There are no chunks, so there's nothing to do.
50        return Ok(None);
51    }
52
53    // Transform the vector into a hashmap, for quick lookup of the predecessors.
54    let chunk_map: HashMap<_, _> = all_chunks.iter().map(|meta| (meta.identifier, meta)).collect();
55
56    // Find a last chunk.
57    let mut iter = all_chunks.iter().filter(|meta| meta.next.is_none());
58    let Some(last) = iter.next() else {
59        return Err(EventCacheError::InvalidLinkedChunkMetadata {
60            details: "no last chunk found".to_owned(),
61        });
62    };
63
64    // There must at most one last chunk.
65    if let Some(other_last) = iter.next() {
66        return Err(EventCacheError::InvalidLinkedChunkMetadata {
67            details: format!(
68                "chunks {} and {} both claim to be last chunks",
69                last.identifier.index(),
70                other_last.identifier.index()
71            ),
72        });
73    }
74
75    // Rewind the chain back to the first chunk, and do some checks at the same
76    // time.
77    let mut seen = HashSet::new();
78    let mut current = last;
79    loop {
80        // If we've already seen this chunk, there's a cycle somewhere.
81        if !seen.insert(current.identifier) {
82            return Err(EventCacheError::InvalidLinkedChunkMetadata {
83                details: format!(
84                    "cycle detected in linked chunk at {}",
85                    current.identifier.index()
86                ),
87            });
88        }
89
90        let Some(prev_id) = current.previous else {
91            // If there's no previous chunk, we're done.
92            if seen.len() != all_chunks.len() {
93                return Err(EventCacheError::InvalidLinkedChunkMetadata {
94                    details: format!(
95                        "linked chunk likely has multiple components: {} chunks seen through the chain of predecessors, but {} expected",
96                        seen.len(),
97                        all_chunks.len()
98                    ),
99                });
100            }
101            break;
102        };
103
104        // If the previous chunk is not in the map, then it's unknown
105        // and missing.
106        let Some(pred_meta) = chunk_map.get(&prev_id) else {
107            return Err(EventCacheError::InvalidLinkedChunkMetadata {
108                details: format!(
109                    "missing predecessor {} chunk for {}",
110                    prev_id.index(),
111                    current.identifier.index()
112                ),
113            });
114        };
115
116        // If the previous chunk isn't connected to the next, then the link is invalid.
117        if pred_meta.next != Some(current.identifier) {
118            return Err(EventCacheError::InvalidLinkedChunkMetadata {
119                details: format!(
120                    "chunk {}'s next ({:?}) doesn't match the current chunk ({})",
121                    pred_meta.identifier.index(),
122                    pred_meta.next.map(|chunk_id| chunk_id.index()),
123                    current.identifier.index()
124                ),
125            });
126        }
127
128        current = *pred_meta;
129    }
130
131    // At this point, `current` is the identifier of the first chunk.
132    //
133    // Reorder the resulting vector, by going through the chain of `next` links, and
134    // swapping items into their final position.
135    //
136    // Invariant in this loop: all items in [0..i[ are in their final, correct
137    // position.
138    let mut current = current.identifier;
139
140    for i in 0..all_chunks.len() {
141        // Find the target metadata.
142        let j = all_chunks
143            .iter()
144            .rev()
145            .position(|meta| meta.identifier == current)
146            .map(|j| all_chunks.len() - 1 - j)
147            .expect("the target chunk must be present in the metadata");
148
149        if i != j {
150            all_chunks.swap(i, j);
151        }
152
153        if let Some(next) = all_chunks[i].next {
154            current = next;
155        }
156    }
157
158    Ok(Some(all_chunks))
159}
160
161/// Propagate linked chunk updates to the store and to the linked chunk update
162/// observers.
163pub(super) async fn send_updates_to_store(
164    store: &EventCacheStoreLockGuard,
165    linked_chunk_id: OwnedLinkedChunkId,
166    linked_chunk_update_sender: &Sender<RoomEventCacheLinkedChunkUpdate>,
167    mut updates: Vec<Update<Event, Gap>>,
168) -> Result<()> {
169    if updates.is_empty() {
170        return Ok(());
171    }
172
173    // Strip relations from updates which insert or replace items.
174    //
175    // The reason we're doing this, is that consumers of the event cache might look
176    // into bundled relations, and assume they're up to date. If we were to keep
177    // the relations in the events, when storing them, then it could be that
178    // they become outdated (as soon as a new relation comes over sync), so we'd
179    // need to update the bundled relations in this case, which would
180    // have a non-negligible cost, as we'd need to look up related events for each
181    // forwarded to a listener.
182    //
183    // As a result, we choose to strip bundled relations from events when we forward
184    // them to the store, and consumers have to explicitly ask for relations.
185    for update in updates.iter_mut() {
186        match update {
187            Update::PushItems { items, .. } => strip_relations_from_events(items),
188            Update::ReplaceItem { item, .. } => strip_relations_from_event(item),
189            // Other update kinds don't involve adding new events.
190            Update::NewItemsChunk { .. }
191            | Update::NewGapChunk { .. }
192            | Update::RemoveChunk(_)
193            | Update::RemoveItem { .. }
194            | Update::DetachLastItems { .. }
195            | Update::StartReattachItems
196            | Update::EndReattachItems
197            | Update::Clear => {}
198        }
199    }
200
201    // Spawn a task to make sure that all the changes are effectively forwarded to
202    // the store, even if the call to this method gets aborted.
203    //
204    // The store cross-process locking involves an actual mutex, which ensures that
205    // storing updates happens in the expected order.
206
207    let store = store.clone();
208    let cloned_updates = updates.clone();
209    let cloned_linked_chunk_id = linked_chunk_id.clone();
210
211    spawn(async move {
212        trace!(updates = ?cloned_updates, "sending linked chunk updates to the store");
213
214        store.handle_linked_chunk_updates(cloned_linked_chunk_id.as_ref(), cloned_updates).await?;
215        trace!("linked chunk updates applied");
216
217        Result::Ok(())
218    })
219    .await
220    .expect("joining failed")?;
221
222    // Forward that the store got updated to observers.
223    let _ = linked_chunk_update_sender
224        .send(RoomEventCacheLinkedChunkUpdate { linked_chunk_id, updates });
225
226    Ok(())
227}
228
229/// Strips the bundled relations from a collection of events.
230fn strip_relations_from_events(items: &mut [Event]) {
231    for ev in items.iter_mut() {
232        strip_relations_from_event(ev);
233    }
234}
235
236/// Strips the bundled relations from an event, if they were present.
237fn strip_relations_from_event(ev: &mut Event) {
238    match &mut ev.kind {
239        TimelineEventKind::Decrypted(decrypted) => {
240            // Remove all information about encryption info for
241            // the bundled events.
242            decrypted.unsigned_encryption_info = None;
243
244            // Remove the `unsigned`/`m.relations` field, if needs be.
245            strip_relations_if_present(&mut decrypted.event);
246        }
247
248        TimelineEventKind::UnableToDecrypt { event, .. }
249        | TimelineEventKind::PlainText { event } => {
250            strip_relations_if_present(event);
251        }
252    }
253}
254
255/// Removes the bundled relations from an event, if they were present.
256///
257/// Only replaces the present if it contained bundled relations.
258fn strip_relations_if_present<T>(event: &mut Raw<T>) {
259    // We're going to get rid of the `unsigned`/`m.relations` field, if it's
260    // present.
261    // Use a closure that returns an option so we can quickly short-circuit.
262    let mut closure = || -> Option<()> {
263        let mut val: serde_json::Value = event.deserialize_as().ok()?;
264        let unsigned = val.get_mut("unsigned")?;
265        let unsigned_obj = unsigned.as_object_mut()?;
266        if unsigned_obj.remove("m.relations").is_some() {
267            *event = Raw::new(&val).ok()?.cast_unchecked();
268        }
269        None
270    };
271    let _ = closure();
272}
273
274/// Find a single event, first in-memory, then in-store.
275pub async fn find_event(
276    event_id: &EventId,
277    room_id: &RoomId,
278    event_linked_chunk: &EventLinkedChunk,
279    store: &EventCacheStoreLockGuard,
280) -> Result<Option<(EventLocation, Event)>> {
281    // There are supposedly fewer events loaded in memory than in the store. Let's
282    // start by looking up in the `EventLinkedChunk`.
283    for (position, event) in event_linked_chunk.revents() {
284        if event.event_id() == Some(event_id) {
285            return Ok(Some((EventLocation::Memory(position), event.clone())));
286        }
287    }
288
289    Ok(store.find_event(room_id, event_id).await?.map(|event| (EventLocation::Store, event)))
290}
291
292/// Find an event and all its relations in the persisted storage.
293///
294/// This goes straight to the database, as a simplification; we don't
295/// expect to need to have to look up in memory events, or that
296/// all the related events are actually loaded.
297///
298/// The related events are sorted like this:
299/// - events saved out-of-band with `save_events` (if this method exists on the
300///   cache calling this function) will be located at the beginning of the
301///   array.
302/// - events present in the linked chunk (be it in memory or in the database)
303///   will be sorted according to their ordering in the linked chunk.
304pub async fn find_event_with_relations(
305    event_id: &EventId,
306    room_id: &RoomId,
307    filters: Option<Vec<RelationType>>,
308    event_linked_chunk: &EventLinkedChunk,
309    store: &EventCacheStoreLockGuard,
310) -> Result<Option<(Event, Vec<Event>)>> {
311    // First, hit storage to get the target event and its related events.
312    let found = store.find_event(room_id, event_id).await?;
313
314    let Some(target) = found else {
315        // We haven't found the event: return early.
316        return Ok(None);
317    };
318
319    // Then, find the transitive closure of all the related events.
320    let related =
321        find_event_relations(event_id, room_id, filters, event_linked_chunk, store).await?;
322
323    Ok(Some((target, related)))
324}
325
326/// Find all relations for an event in the persisted storage.
327///
328/// This goes straight to the database, as a simplification; we don't
329/// expect to need to have to look up in memory events, or that
330/// all the related events are actually loaded.
331///
332/// The related events are sorted like this:
333/// - events saved out-of-band with `save_events` (if this method exists on the
334///   cache calling this function) will be located at the beginning of the
335///   array.
336/// - events present in the linked chunk (be it in memory or in the database)
337///   will be sorted according to their ordering in the linked chunk.
338pub async fn find_event_relations(
339    event_id: &EventId,
340    room_id: &RoomId,
341    filters: Option<Vec<RelationType>>,
342    event_linked_chunk: &EventLinkedChunk,
343    store: &EventCacheStoreLockGuard,
344) -> Result<Vec<Event>> {
345    // Initialize the stack with all the related events, to find the
346    // transitive closure of all the related events.
347    let mut related = store.find_event_relations(room_id, event_id, filters.as_deref()).await?;
348    let mut stack = related
349        .iter()
350        .filter_map(|(event, _pos)| event.event_id().map(ToOwned::to_owned))
351        .collect::<Vec<_>>();
352
353    // Also keep track of already seen events, in case there's a loop in the
354    // relation graph.
355    let mut already_seen = HashSet::new();
356    already_seen.insert(event_id.to_owned());
357
358    let mut num_iters = 1;
359
360    // Find the related event for each previously-related event.
361    while let Some(event_id) = stack.pop() {
362        if !already_seen.insert(event_id.clone()) {
363            // Skip events we've already seen.
364            continue;
365        }
366
367        let other_related =
368            store.find_event_relations(room_id, &event_id, filters.as_deref()).await?;
369
370        stack.extend(
371            other_related
372                .iter()
373                .filter_map(|(event, _pos)| event.event_id().map(ToOwned::to_owned)),
374        );
375        related.extend(other_related);
376
377        num_iters += 1;
378    }
379
380    trace!(num_related = %related.len(), num_iters, "computed transitive closure of related events");
381
382    // Sort the results by their positions in the linked chunk, if available.
383    //
384    // If an event doesn't have a known position, it goes to the start of the array.
385    related.sort_by(|(_, lhs), (_, rhs)| {
386        use std::cmp::Ordering;
387
388        match (lhs, rhs) {
389            (None, None) => Ordering::Equal,
390            (None, Some(_)) => Ordering::Less,
391            (Some(_), None) => Ordering::Greater,
392            (Some(lhs), Some(rhs)) => {
393                let lhs = event_linked_chunk.event_order(*lhs);
394                let rhs = event_linked_chunk.event_order(*rhs);
395
396                // The events should have a definite position, but in the case they don't,
397                // still consider that not having a position means you'll end at the start
398                // of the array.
399                match (lhs, rhs) {
400                    (None, None) => Ordering::Equal,
401                    (None, Some(_)) => Ordering::Less,
402                    (Some(_), None) => Ordering::Greater,
403                    (Some(lhs), Some(rhs)) => lhs.cmp(&rhs),
404                }
405            }
406        }
407    });
408
409    // Keep only the events, not their positions.
410    let related = related.into_iter().map(|(event, _pos)| event).collect();
411
412    Ok(related)
413}