Skip to main content

matrix_sdk_indexeddb/event_cache_store/
transaction.rs

1// Copyright 2025 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::ops::Deref;
16
17use indexed_db_futures::transaction as inner;
18use matrix_sdk_base::{
19    event_cache::{Event as RawEvent, Gap as RawGap},
20    linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, RawChunk},
21};
22use ruma::{EventId, RoomId, events::relation::RelationType};
23use serde::{Serialize, de::DeserializeOwned};
24
25use crate::{
26    error::AsyncErrorDeps,
27    event_cache_store::{
28        serializer::indexed_types::{
29            IndexedChunk, IndexedChunkIdKey, IndexedEvent, IndexedEventIdKey,
30            IndexedEventPositionKey, IndexedEventRelationKey, IndexedEventRoomKey, IndexedGapIdKey,
31            IndexedLease, IndexedLeaseIdKey, IndexedNextChunkIdKey,
32        },
33        types::{Chunk, ChunkType, Event, Gap, Lease, Position},
34    },
35    serializer::indexed_type::{
36        IndexedTypeSerializer,
37        range::IndexedKeyRange,
38        traits::{Indexed, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds},
39    },
40    transaction::{Transaction, TransactionError},
41};
42
43/// Represents an IndexedDB transaction, but provides a convenient interface for
44/// performing operations relevant to the IndexedDB implementation of
45/// [`EventCacheStore`](matrix_sdk_base::event_cache::store::EventCacheStore).
46pub struct IndexeddbEventCacheStoreTransaction<'a> {
47    transaction: Transaction<'a>,
48}
49
50impl<'a> Deref for IndexeddbEventCacheStoreTransaction<'a> {
51    type Target = Transaction<'a>;
52
53    fn deref(&self) -> &Self::Target {
54        &self.transaction
55    }
56}
57
58impl<'a> IndexeddbEventCacheStoreTransaction<'a> {
59    pub fn new(transaction: inner::Transaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self {
60        Self { transaction: Transaction::new(transaction, serializer) }
61    }
62
63    /// Commit all operations tracked in this transaction to IndexedDB.
64    pub async fn commit(self) -> Result<(), TransactionError> {
65        self.transaction.commit().await
66    }
67
68    /// Query IndexedDB for all items matching the given linked chunk id by key
69    /// `K`
70    pub async fn get_items_by_linked_chunk_id<'b, T, K>(
71        &self,
72        linked_chunk_id: LinkedChunkId<'b>,
73    ) -> Result<Vec<T>, TransactionError>
74    where
75        T: Indexed,
76        T::IndexedType: DeserializeOwned,
77        T::Error: AsyncErrorDeps,
78        K: IndexedPrefixKeyBounds<T, LinkedChunkId<'b>> + Serialize,
79    {
80        self.get_items_by_key::<T, K>(IndexedKeyRange::all_with_prefix(
81            linked_chunk_id,
82            self.serializer().inner(),
83        ))
84        .await
85    }
86
87    /// Query IndexedDB for all items of type `T` by key `K` in the given room
88    pub async fn get_items_in_room<'b, T, K>(
89        &self,
90        room_id: &'b RoomId,
91    ) -> Result<Vec<T>, TransactionError>
92    where
93        T: Indexed,
94        T::IndexedType: DeserializeOwned,
95        T::Error: AsyncErrorDeps,
96        K: IndexedPrefixKeyBounds<T, &'b RoomId> + Serialize,
97    {
98        self.get_items_by_key::<T, K>(IndexedKeyRange::all_with_prefix(
99            room_id,
100            self.serializer().inner(),
101        ))
102        .await
103    }
104
105    /// Query IndexedDB for the number of items matching the given linked chunk
106    /// id.
107    pub async fn get_items_count_by_linked_chunk_id<'b, T, K>(
108        &self,
109        linked_chunk_id: LinkedChunkId<'b>,
110    ) -> Result<usize, TransactionError>
111    where
112        T: Indexed,
113        T::IndexedType: DeserializeOwned,
114        T::Error: AsyncErrorDeps,
115        K: IndexedPrefixKeyBounds<T, LinkedChunkId<'b>> + Serialize,
116    {
117        self.get_items_count_by_key::<T, K>(IndexedKeyRange::all_with_prefix(
118            linked_chunk_id,
119            self.serializer().inner(),
120        ))
121        .await
122    }
123
124    /// Delete all items of type `T` by key `K` associated with the given linked
125    /// chunk id from IndexedDB
126    pub async fn delete_items_by_linked_chunk_id<'b, T, K>(
127        &self,
128        linked_chunk_id: LinkedChunkId<'b>,
129    ) -> Result<(), TransactionError>
130    where
131        T: Indexed,
132        K: IndexedPrefixKeyBounds<T, LinkedChunkId<'b>> + Serialize,
133    {
134        self.delete_items_by_key::<T, K>(IndexedKeyRange::all_with_prefix(
135            linked_chunk_id,
136            self.serializer().inner(),
137        ))
138        .await
139    }
140
141    /// Query IndexedDB for the lease that matches the given key `id`. If more
142    /// than one lease is found, an error is returned.
143    pub async fn get_lease_by_id(&self, id: &str) -> Result<Option<Lease>, TransactionError> {
144        self.get_item_by_key_components::<Lease, IndexedLeaseIdKey>(id).await
145    }
146
147    /// Puts a lease into IndexedDB. If an event with the same key already
148    /// exists, it will be overwritten. When the item is successfully put, the
149    /// function returns the intermediary type [`IndexedLease`] in case
150    /// inspection is needed.
151    pub async fn put_lease(&self, lease: &Lease) -> Result<IndexedLease, TransactionError> {
152        self.put_item(lease).await
153    }
154
155    /// Query IndexedDB for chunks that match the given chunk identifier and the
156    /// given linked chunk id. If more than one item is found, an error is
157    /// returned.
158    pub async fn get_chunk_by_id(
159        &self,
160        linked_chunk_id: LinkedChunkId<'_>,
161        chunk_id: ChunkIdentifier,
162    ) -> Result<Option<Chunk>, TransactionError> {
163        self.get_item_by_key_components::<Chunk, IndexedChunkIdKey>((linked_chunk_id, chunk_id))
164            .await
165    }
166
167    /// Query IndexedDB for chunks such that the next chunk matches the given
168    /// chunk identifier and the given linked chunk id. If more than one item is
169    /// found, an error is returned.
170    pub async fn get_chunk_by_next_chunk_id(
171        &self,
172        linked_chunk_id: LinkedChunkId<'_>,
173        next_chunk_id: Option<ChunkIdentifier>,
174    ) -> Result<Option<Chunk>, TransactionError> {
175        self.get_item_by_key_components::<Chunk, IndexedNextChunkIdKey>((
176            linked_chunk_id,
177            next_chunk_id,
178        ))
179        .await
180    }
181
182    /// Query IndexedDB for all chunks matching the given linked chunk id
183    pub async fn get_chunks_by_linked_chunk_id(
184        &self,
185        linked_chunk_id: LinkedChunkId<'_>,
186    ) -> Result<Vec<Chunk>, TransactionError> {
187        self.get_items_by_linked_chunk_id::<Chunk, IndexedChunkIdKey>(linked_chunk_id).await
188    }
189
190    /// Query IndexedDB for the number of chunks matching the given linked chunk
191    /// id.
192    pub async fn get_chunks_count_by_linked_chunk_id(
193        &self,
194        linked_chunk_id: LinkedChunkId<'_>,
195    ) -> Result<usize, TransactionError> {
196        self.get_items_count_by_linked_chunk_id::<Chunk, IndexedChunkIdKey>(linked_chunk_id).await
197    }
198
199    /// Query IndexedDB for the chunk with the maximum key matching the given
200    /// linked chunk id.
201    pub async fn get_max_chunk_by_id(
202        &self,
203        linked_chunk_id: LinkedChunkId<'_>,
204    ) -> Result<Option<Chunk>, TransactionError> {
205        let range = IndexedKeyRange::all_with_prefix::<Chunk, _>(
206            linked_chunk_id,
207            self.serializer().inner(),
208        );
209        self.get_max_item_by_key::<Chunk, IndexedChunkIdKey>(range).await
210    }
211
212    /// Query IndexedDB for given chunk matching the given linked chunk id and
213    /// additionally query for events or gap, depending on chunk type, in
214    /// order to construct the full chunk.
215    pub async fn load_chunk_by_id(
216        &self,
217        linked_chunk_id: LinkedChunkId<'_>,
218        chunk_id: ChunkIdentifier,
219    ) -> Result<Option<RawChunk<RawEvent, RawGap>>, TransactionError> {
220        if let Some(chunk) = self.get_chunk_by_id(linked_chunk_id, chunk_id).await? {
221            let content = match chunk.chunk_type {
222                ChunkType::Event => {
223                    let events = self
224                        .get_events_by_chunk(
225                            linked_chunk_id,
226                            ChunkIdentifier::new(chunk.identifier),
227                        )
228                        .await?
229                        .into_iter()
230                        .map(RawEvent::from)
231                        .collect();
232                    ChunkContent::Items(events)
233                }
234                ChunkType::Gap => {
235                    let gap = self
236                        .get_gap_by_id(linked_chunk_id, ChunkIdentifier::new(chunk.identifier))
237                        .await?
238                        .ok_or(TransactionError::ItemNotFound)?;
239                    ChunkContent::Gap(RawGap { token: gap.token })
240                }
241            };
242            return Ok(Some(RawChunk {
243                identifier: ChunkIdentifier::new(chunk.identifier),
244                content,
245                previous: chunk.previous.map(ChunkIdentifier::new),
246                next: chunk.next.map(ChunkIdentifier::new),
247            }));
248        }
249        Ok(None)
250    }
251
252    /// Add a chunk and ensure that the next and previous
253    /// chunks are properly linked to the chunk being added. If a chunk with
254    /// the same identifier already exists, the given chunk will be
255    /// rejected. When the item is successfully added, the
256    /// function returns the intermediary type [`IndexedChunk`] in case
257    /// inspection is needed.
258    pub async fn add_chunk(&self, chunk: &Chunk) -> Result<IndexedChunk, TransactionError> {
259        let indexed = self.add_item(chunk).await?;
260        if let Some(previous) = chunk.previous {
261            let previous_identifier = ChunkIdentifier::new(previous);
262            let mut previous_chunk = self
263                .get_chunk_by_id(chunk.linked_chunk_id.as_ref(), previous_identifier)
264                .await?
265                .ok_or(TransactionError::ItemNotFound)?;
266            previous_chunk.next = Some(chunk.identifier);
267            self.put_item(&previous_chunk).await?;
268        }
269        if let Some(next) = chunk.next {
270            let next_identifier = ChunkIdentifier::new(next);
271            let mut next_chunk = self
272                .get_chunk_by_id(chunk.linked_chunk_id.as_ref(), next_identifier)
273                .await?
274                .ok_or(TransactionError::ItemNotFound)?;
275            next_chunk.previous = Some(chunk.identifier);
276            self.put_item(&next_chunk).await?;
277        }
278        Ok(indexed)
279    }
280
281    /// Delete chunk that matches the given id and the given linked chunk id and
282    /// ensure that the next and previous chunk are updated to link to one
283    /// another. Additionally, ensure that events and gaps in the given
284    /// chunk are also deleted.
285    pub async fn delete_chunk_by_id(
286        &self,
287        linked_chunk_id: LinkedChunkId<'_>,
288        chunk_id: ChunkIdentifier,
289    ) -> Result<(), TransactionError> {
290        if let Some(chunk) = self.get_chunk_by_id(linked_chunk_id, chunk_id).await? {
291            if let Some(previous) = chunk.previous {
292                let previous_identifier = ChunkIdentifier::new(previous);
293                if let Some(mut previous_chunk) =
294                    self.get_chunk_by_id(linked_chunk_id, previous_identifier).await?
295                {
296                    previous_chunk.next = chunk.next;
297                    self.put_item(&previous_chunk).await?;
298                }
299            }
300            if let Some(next) = chunk.next {
301                let next_identifier = ChunkIdentifier::new(next);
302                if let Some(mut next_chunk) =
303                    self.get_chunk_by_id(linked_chunk_id, next_identifier).await?
304                {
305                    next_chunk.previous = chunk.previous;
306                    self.put_item(&next_chunk).await?;
307                }
308            }
309            self.delete_item_by_key::<Chunk, IndexedChunkIdKey>((linked_chunk_id, chunk_id))
310                .await?;
311            match chunk.chunk_type {
312                ChunkType::Event => {
313                    self.delete_events_by_chunk(linked_chunk_id, chunk_id).await?;
314                }
315                ChunkType::Gap => {
316                    self.delete_gap_by_id(linked_chunk_id, chunk_id).await?;
317                }
318            }
319        }
320        Ok(())
321    }
322
323    /// Delete all chunks associated with the given linked chunk id
324    pub async fn delete_chunks_by_linked_chunk_id(
325        &self,
326        linked_chunk_id: LinkedChunkId<'_>,
327    ) -> Result<(), TransactionError> {
328        self.delete_items_by_linked_chunk_id::<Chunk, IndexedChunkIdKey>(linked_chunk_id).await
329    }
330
331    /// Query IndexedDB for events that match the given event id and the given
332    /// linked chunk id. If more than one item is found, an error is returned.
333    pub async fn get_event_by_id(
334        &self,
335        linked_chunk_id: LinkedChunkId<'_>,
336        event_id: &EventId,
337    ) -> Result<Option<Event>, TransactionError> {
338        let key = self.serializer().encode_key((linked_chunk_id, event_id));
339        self.get_item_by_key::<Event, IndexedEventIdKey>(key).await
340    }
341
342    /// Query IndexedDB for events that match the given event id in the given
343    /// room. If more than one item is found, an error is returned.
344    pub async fn get_event_by_room(
345        &self,
346        room_id: &RoomId,
347        event_id: &EventId,
348    ) -> Result<Option<Event>, TransactionError> {
349        let key = self.serializer().encode_key((room_id, event_id));
350        self.get_item_by_key::<Event, IndexedEventRoomKey>(key).await
351    }
352
353    /// Query IndexedDB for events that match the given event id in the given
354    /// room.
355    pub async fn get_events_by_room(
356        &self,
357        room_id: &RoomId,
358        event_id: &EventId,
359    ) -> Result<Vec<Event>, TransactionError> {
360        let key: IndexedEventRoomKey = self.serializer().encode_key((room_id, event_id));
361        self.get_items_by_key::<Event, IndexedEventRoomKey>(key).await
362    }
363
364    /// Query IndexedDB for events that are in the given
365    /// room.
366    pub async fn get_room_events(&self, room_id: &RoomId) -> Result<Vec<Event>, TransactionError> {
367        self.get_items_in_room::<Event, IndexedEventRoomKey>(room_id).await
368    }
369
370    /// Query IndexedDB for events in the given chunk matching the given linked
371    /// chunk id.
372    pub async fn get_events_by_chunk(
373        &self,
374        linked_chunk_id: LinkedChunkId<'_>,
375        chunk_id: ChunkIdentifier,
376    ) -> Result<Vec<Event>, TransactionError> {
377        let range = IndexedKeyRange::all_with_prefix(
378            (linked_chunk_id, chunk_id),
379            self.serializer().inner(),
380        );
381        self.get_items_by_key::<Event, IndexedEventPositionKey>(range).await
382    }
383
384    /// Query IndexedDB for number of events in the given chunk matching the
385    /// given linked chunk id.
386    pub async fn get_events_count_by_chunk(
387        &self,
388        linked_chunk_id: LinkedChunkId<'_>,
389        chunk_id: ChunkIdentifier,
390    ) -> Result<usize, TransactionError> {
391        let range = IndexedKeyRange::all_with_prefix(
392            (linked_chunk_id, chunk_id),
393            self.serializer().inner(),
394        );
395        self.get_items_count_by_key::<Event, IndexedEventPositionKey>(range).await
396    }
397
398    /// Query IndexedDB for events that match the given relation range in the
399    /// given room.
400    pub async fn get_events_by_relation(
401        &self,
402        room_id: &RoomId,
403        range: impl Into<IndexedKeyRange<(&EventId, &RelationType)>>,
404    ) -> Result<Vec<Event>, TransactionError> {
405        let range = range
406            .into()
407            .map(|(event_id, relation_type)| (room_id, event_id, relation_type))
408            .encoded(self.serializer().inner());
409        self.get_items_by_key::<Event, IndexedEventRelationKey>(range).await
410    }
411
412    /// Query IndexedDB for events that are related to the given event in the
413    /// given room.
414    pub async fn get_events_by_related_event(
415        &self,
416        room_id: &RoomId,
417        related_event_id: &EventId,
418    ) -> Result<Vec<Event>, TransactionError> {
419        let range = IndexedKeyRange::all_with_prefix(
420            (room_id, related_event_id),
421            self.serializer().inner(),
422        );
423        self.get_items_by_key::<Event, IndexedEventRelationKey>(range).await
424    }
425
426    /// Puts an event in IndexedDB. If an event with the same key already
427    /// exists, it will be overwritten. When the item is successfully put, the
428    /// function returns the intermediary type [`IndexedEvent`] in case
429    /// inspection is needed.
430    pub async fn put_event(&self, event: &Event) -> Result<IndexedEvent, TransactionError> {
431        if let Some(position) = event.position() {
432            // For some reason, we can't simply replace an event with `put_item`
433            // because we can get an error stating that the data violates a uniqueness
434            // constraint on the `events_position` index. This is NOT expected, but
435            // it is not clear if this improperly implemented in the browser or the
436            // library we are using.
437            //
438            // As a workaround, if the event has a position, we delete it first and
439            // then call `put_item`. This should be fine as it all happens within the
440            // context of a single transaction.
441            self.delete_event_by_position(event.linked_chunk_id(), position).await?;
442        }
443        self.put_item(event).await
444    }
445
446    /// Delete events in the given position range matching the given linked
447    /// chunk id
448    pub async fn delete_events_by_position(
449        &self,
450        linked_chunk_id: LinkedChunkId<'_>,
451        range: impl Into<IndexedKeyRange<Position>>,
452    ) -> Result<(), TransactionError> {
453        self.delete_items_by_key_components::<Event, IndexedEventPositionKey>(
454            range.into().map(|position| (linked_chunk_id, position)),
455        )
456        .await
457    }
458
459    /// Delete event in the given position matching the given linked chunk id
460    pub async fn delete_event_by_position(
461        &self,
462        linked_chunk_id: LinkedChunkId<'_>,
463        position: Position,
464    ) -> Result<(), TransactionError> {
465        self.delete_item_by_key::<Event, IndexedEventPositionKey>((linked_chunk_id, position)).await
466    }
467
468    /// Delete events in the given chunk matching the given linked chunk id
469    pub async fn delete_events_by_chunk(
470        &self,
471        linked_chunk_id: LinkedChunkId<'_>,
472        chunk_id: ChunkIdentifier,
473    ) -> Result<(), TransactionError> {
474        let range = IndexedKeyRange::all_with_prefix(
475            (linked_chunk_id, chunk_id),
476            self.serializer().inner(),
477        );
478        self.delete_items_by_key::<Event, IndexedEventPositionKey>(range).await
479    }
480
481    /// Delete events matching the given linked chunk id starting from the given
482    /// position until the end of the chunk
483    pub async fn delete_events_by_chunk_from_index(
484        &self,
485        linked_chunk_id: LinkedChunkId<'_>,
486        position: Position,
487    ) -> Result<(), TransactionError> {
488        let lower = (linked_chunk_id, position);
489        let upper = IndexedEventPositionKey::upper_key_components_with_prefix((
490            linked_chunk_id,
491            ChunkIdentifier::new(position.chunk_identifier),
492        ));
493        let range = IndexedKeyRange::Bound(lower, upper).map(|(_, position)| position);
494        self.delete_events_by_position(linked_chunk_id, range).await
495    }
496
497    /// Delete all events matching the given linked chunk id
498    pub async fn delete_events_by_linked_chunk_id(
499        &self,
500        linked_chunk_id: LinkedChunkId<'_>,
501    ) -> Result<(), TransactionError> {
502        self.delete_items_by_linked_chunk_id::<Event, IndexedEventIdKey>(linked_chunk_id).await
503    }
504
505    /// Query IndexedDB for the gap in the given chunk matching the given linked
506    /// chunk id.
507    pub async fn get_gap_by_id(
508        &self,
509        linked_chunk_id: LinkedChunkId<'_>,
510        chunk_id: ChunkIdentifier,
511    ) -> Result<Option<Gap>, TransactionError> {
512        self.get_item_by_key_components::<Gap, IndexedGapIdKey>((linked_chunk_id, chunk_id)).await
513    }
514
515    /// Delete gap that matches the given chunk identifier and the given linked
516    /// chunk id
517    pub async fn delete_gap_by_id(
518        &self,
519        linked_chunk_id: LinkedChunkId<'_>,
520        chunk_id: ChunkIdentifier,
521    ) -> Result<(), TransactionError> {
522        self.delete_item_by_key::<Gap, IndexedGapIdKey>((linked_chunk_id, chunk_id)).await
523    }
524
525    /// Delete all gaps matching the given linked chunk id
526    pub async fn delete_gaps_by_linked_chunk_id(
527        &self,
528        linked_chunk_id: LinkedChunkId<'_>,
529    ) -> Result<(), TransactionError> {
530        self.delete_items_by_linked_chunk_id::<Gap, IndexedGapIdKey>(linked_chunk_id).await
531    }
532}