Skip to main content

matrix_sdk_indexeddb/event_cache_store/
mod.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
15#![cfg_attr(not(test), allow(unused))]
16
17use std::{
18    collections::{HashMap, HashSet},
19    rc::Rc,
20    time::Duration,
21};
22
23use indexed_db_futures::{Build, database::Database};
24#[cfg(target_family = "wasm")]
25use matrix_sdk_base::{
26    cross_process_lock::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION},
27    event_cache::thread::ThreadInfo,
28};
29use matrix_sdk_base::{
30    event_cache::{Event, Gap, store::EventCacheStore},
31    linked_chunk::{
32        ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position,
33        RawChunk, Update,
34    },
35    timer,
36};
37use ruma::{
38    EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, events::relation::RelationType,
39};
40use tracing::{error, instrument, trace};
41use web_sys::IdbTransactionMode;
42
43use crate::{
44    event_cache_store::{
45        migrations::current::keys,
46        transaction::IndexeddbEventCacheStoreTransaction,
47        types::{ChunkType, InBandEvent, Lease, OutOfBandEvent, Thread},
48    },
49    serializer::indexed_type::{IndexedTypeSerializer, traits::Indexed},
50    transaction::TransactionError,
51};
52
53mod builder;
54mod error;
55#[cfg(all(test, target_family = "wasm"))]
56mod integration_tests;
57mod migrations;
58mod serializer;
59mod transaction;
60mod types;
61
62pub use builder::IndexeddbEventCacheStoreBuilder;
63pub use error::IndexeddbEventCacheStoreError;
64
65/// A type for providing an IndexedDB implementation of [`EventCacheStore`][1].
66/// This is meant to be used as a backend to [`EventCacheStore`][1] in browser
67/// contexts.
68///
69/// [1]: matrix_sdk_base::event_cache::store::EventCacheStore
70#[derive(Debug, Clone)]
71pub struct IndexeddbEventCacheStore {
72    // A handle to the IndexedDB database
73    inner: Rc<Database>,
74    // A serializer with functionality tailored to `IndexeddbEventCacheStore`
75    serializer: IndexedTypeSerializer,
76}
77
78impl IndexeddbEventCacheStore {
79    /// Provides a type with which to conveniently build an
80    /// [`IndexeddbEventCacheStore`]
81    pub fn builder() -> IndexeddbEventCacheStoreBuilder {
82        IndexeddbEventCacheStoreBuilder::default()
83    }
84
85    /// Initializes a new transaction on the underlying IndexedDB database and
86    /// returns a handle which can be used to combine database operations into
87    /// an atomic unit.
88    pub fn transaction<'a>(
89        &'a self,
90        stores: &[&str],
91        mode: IdbTransactionMode,
92    ) -> Result<IndexeddbEventCacheStoreTransaction<'a>, IndexeddbEventCacheStoreError> {
93        Ok(IndexeddbEventCacheStoreTransaction::new(
94            self.inner
95                .transaction(stores)
96                .with_mode(mode)
97                .build()
98                .map_err(TransactionError::from)?,
99            &self.serializer,
100        ))
101    }
102}
103
104#[cfg(target_family = "wasm")]
105#[async_trait::async_trait(?Send)]
106impl EventCacheStore for IndexeddbEventCacheStore {
107    type Error = IndexeddbEventCacheStoreError;
108
109    #[instrument(skip(self))]
110    async fn try_take_leased_lock(
111        &self,
112        lease_duration_ms: u32,
113        key: &str,
114        holder: &str,
115    ) -> Result<Option<CrossProcessLockGeneration>, IndexeddbEventCacheStoreError> {
116        let transaction =
117            self.transaction(&[Lease::OBJECT_STORE], IdbTransactionMode::Readwrite)?;
118
119        let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());
120        let expiration = now + Duration::from_millis(lease_duration_ms.into());
121
122        let lease = match transaction.get_lease_by_id(key).await? {
123            Some(mut lease) => {
124                if lease.holder == holder {
125                    // We had the lease before, extend it.
126                    lease.expiration = expiration;
127
128                    Some(lease)
129                } else {
130                    // We didn't have it.
131                    if lease.expiration < now {
132                        // Steal it!
133                        lease.holder = holder.to_owned();
134                        lease.expiration = expiration;
135                        lease.generation += 1;
136
137                        Some(lease)
138                    } else {
139                        // We tried our best.
140                        None
141                    }
142                }
143            }
144            None => {
145                let lease = Lease {
146                    key: key.to_owned(),
147                    holder: holder.to_owned(),
148                    expiration,
149                    generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
150                };
151
152                Some(lease)
153            }
154        };
155
156        Ok(if let Some(lease) = lease {
157            transaction.put_lease(&lease)?;
158            transaction.commit().await?;
159
160            Some(lease.generation)
161        } else {
162            None
163        })
164    }
165
166    #[instrument(skip(self, updates))]
167    async fn handle_linked_chunk_updates(
168        &self,
169        linked_chunk_id: LinkedChunkId<'_>,
170        updates: Vec<Update<Event, Gap>>,
171    ) -> Result<(), IndexeddbEventCacheStoreError> {
172        let _timer = timer!("method");
173
174        let transaction = self.transaction(
175            &[keys::LINKED_CHUNKS, keys::GAPS, keys::EVENTS],
176            IdbTransactionMode::Readwrite,
177        )?;
178
179        // Test whether necessary components of an event are present in the
180        // underlying structure
181        let is_complete_event = |event: &Event| {
182            let Some(event_id) = event.event_id() else {
183                error!("Found event with no ID");
184                return false;
185            };
186            if event.kind.event_type().is_none() {
187                error!(%event_id, "Found an event with no event type");
188                return false;
189            }
190            true
191        };
192
193        for update in updates {
194            match update {
195                Update::NewItemsChunk { previous, new, next } => {
196                    trace!(%linked_chunk_id, "Inserting new chunk (prev={previous:?}, new={new:?}, next={next:?})");
197                    transaction
198                        .add_chunk(&types::Chunk {
199                            linked_chunk_id: linked_chunk_id.to_owned(),
200                            identifier: new.index(),
201                            previous: previous.map(|i| i.index()),
202                            next: next.map(|i| i.index()),
203                            chunk_type: ChunkType::Event,
204                        })
205                        .await?;
206                }
207                Update::NewGapChunk { previous, new, next, gap } => {
208                    trace!(%linked_chunk_id, "Inserting new gap (prev={previous:?}, new={new:?}, next={next:?})");
209                    transaction.add_item(&types::Gap {
210                        linked_chunk_id: linked_chunk_id.to_owned(),
211                        chunk_identifier: new.index(),
212                        token: gap.token,
213                    })?;
214                    transaction
215                        .add_chunk(&types::Chunk {
216                            linked_chunk_id: linked_chunk_id.to_owned(),
217                            identifier: new.index(),
218                            previous: previous.map(|i| i.index()),
219                            next: next.map(|i| i.index()),
220                            chunk_type: ChunkType::Gap,
221                        })
222                        .await?;
223                }
224                Update::RemoveChunk(chunk_id) => {
225                    trace!(%linked_chunk_id, "Removing chunk {chunk_id:?}");
226                    transaction.delete_chunk_by_id(linked_chunk_id, chunk_id).await?;
227                }
228                Update::PushItems { at, items } => {
229                    let chunk_identifier = at.chunk_identifier().index();
230
231                    trace!(%linked_chunk_id, "pushing {} items @ {chunk_identifier}", items.len());
232
233                    for (i, item) in items.into_iter().filter(is_complete_event).enumerate() {
234                        transaction
235                            .add_event(&types::Event::InBand(InBandEvent {
236                                linked_chunk_id: linked_chunk_id.to_owned(),
237                                content: item,
238                                position: types::Position {
239                                    chunk_identifier,
240                                    index: at.index() + i,
241                                },
242                            }))
243                            .await?;
244                    }
245                }
246                Update::ReplaceItem { at, item } => {
247                    let chunk_id = at.chunk_identifier().index();
248                    let index = at.index();
249
250                    trace!(%linked_chunk_id, "replacing item @ {chunk_id}:{index}");
251
252                    if !is_complete_event(&item) {
253                        continue;
254                    }
255
256                    transaction
257                        .put_event(&types::Event::InBand(InBandEvent {
258                            linked_chunk_id: linked_chunk_id.to_owned(),
259                            content: item,
260                            position: at.into(),
261                        }))
262                        .await?;
263                }
264                Update::RemoveItem { at } => {
265                    let chunk_id = at.chunk_identifier().index();
266                    let index = at.index();
267
268                    trace!(%linked_chunk_id, "removing item @ {chunk_id}:{index}");
269
270                    transaction.delete_event_by_position(linked_chunk_id, at.into()).await?;
271                }
272                Update::DetachLastItems { at } => {
273                    let chunk_id = at.chunk_identifier().index();
274                    let index = at.index();
275
276                    trace!(%linked_chunk_id, "detaching last items @ {chunk_id}:{index}");
277
278                    transaction
279                        .delete_events_by_chunk_from_index(linked_chunk_id, at.into())
280                        .await?;
281                }
282                Update::StartReattachItems | Update::EndReattachItems => {
283                    // Nothing? See sqlite implementation
284                }
285                Update::Clear => {
286                    trace!(%linked_chunk_id, "clearing room");
287                    transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
288                    transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
289                    transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
290                }
291            }
292        }
293        transaction.commit().await?;
294        Ok(())
295    }
296
297    #[instrument(skip(self))]
298    async fn load_all_chunks(
299        &self,
300        linked_chunk_id: LinkedChunkId<'_>,
301    ) -> Result<Vec<RawChunk<Event, Gap>>, IndexeddbEventCacheStoreError> {
302        let _ = timer!("method");
303
304        let transaction = self.transaction(
305            &[keys::LINKED_CHUNKS, keys::GAPS, keys::EVENTS],
306            IdbTransactionMode::Readonly,
307        )?;
308
309        let mut raw_chunks = Vec::new();
310        let chunks = transaction.get_chunks_by_linked_chunk_id(linked_chunk_id).await?;
311        for chunk in chunks {
312            if let Some(raw_chunk) = transaction
313                .load_chunk_by_id(linked_chunk_id, ChunkIdentifier::new(chunk.identifier))
314                .await?
315            {
316                raw_chunks.push(raw_chunk);
317            }
318        }
319        Ok(raw_chunks)
320    }
321
322    #[instrument(skip(self))]
323    async fn load_all_chunks_metadata(
324        &self,
325        linked_chunk_id: LinkedChunkId<'_>,
326    ) -> Result<Vec<ChunkMetadata>, IndexeddbEventCacheStoreError> {
327        // TODO: This call could possibly take a very long time and the amount
328        // of time increases linearly with the number of chunks it needs to load
329        // from the database. This will likely require some refactoring to deal
330        // with performance issues.
331        //
332        // For details on the performance penalties associated with this call,
333        // see https://github.com/matrix-org/matrix-rust-sdk/pull/5407.
334        //
335        // For how this was improved in the SQLite implementation, see
336        // https://github.com/matrix-org/matrix-rust-sdk/pull/5382.
337        let _ = timer!("method");
338
339        let transaction = self.transaction(
340            &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
341            IdbTransactionMode::Readonly,
342        )?;
343
344        let mut raw_chunks = Vec::new();
345        let chunks = transaction.get_chunks_by_linked_chunk_id(linked_chunk_id).await?;
346        for chunk in chunks {
347            let chunk_id = ChunkIdentifier::new(chunk.identifier);
348            let num_items =
349                transaction.get_events_count_by_chunk(linked_chunk_id, chunk_id).await?;
350            raw_chunks.push(ChunkMetadata {
351                num_items,
352                previous: chunk.previous.map(ChunkIdentifier::new),
353                identifier: ChunkIdentifier::new(chunk.identifier),
354                next: chunk.next.map(ChunkIdentifier::new),
355            });
356        }
357        Ok(raw_chunks)
358    }
359
360    #[instrument(skip(self))]
361    async fn load_last_chunk(
362        &self,
363        linked_chunk_id: LinkedChunkId<'_>,
364    ) -> Result<
365        (Option<RawChunk<Event, Gap>>, ChunkIdentifierGenerator),
366        IndexeddbEventCacheStoreError,
367    > {
368        let _timer = timer!("method");
369
370        let transaction = self.transaction(
371            &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
372            IdbTransactionMode::Readonly,
373        )?;
374
375        if transaction.get_chunks_count_by_linked_chunk_id(linked_chunk_id).await? == 0 {
376            return Ok((None, ChunkIdentifierGenerator::new_from_scratch()));
377        }
378        // Now that we know we have some chunks in the room, we query IndexedDB
379        // for the last chunk in the room by getting the chunk which does not
380        // have a next chunk.
381        match transaction.get_chunk_by_next_chunk_id(linked_chunk_id, None).await {
382            Err(TransactionError::ItemIsNotUnique) => {
383                // If there are multiple chunks that do not have a next chunk,
384                // that means we have more than one last chunk, which means that
385                // we have more than one list in the room.
386                Err(IndexeddbEventCacheStoreError::ChunksContainDisjointLists)
387            }
388            Err(e) => {
389                // There was some error querying IndexedDB, but it is not
390                // necessarily a violation of our data constraints.
391                Err(e.into())
392            }
393            Ok(None) => {
394                // If there is no chunk without a next chunk, that means every
395                // chunk points to another chunk, which means that we have a
396                // cycle in our list.
397                Err(IndexeddbEventCacheStoreError::ChunksContainCycle)
398            }
399            Ok(Some(last_chunk)) => {
400                let last_chunk_identifier = ChunkIdentifier::new(last_chunk.identifier);
401
402                let (last_raw_chunk, max_chunk_id) = futures_util::future::try_join(
403                    transaction.load_chunk_by_id(linked_chunk_id, last_chunk_identifier),
404                    transaction.get_max_chunk_by_id(linked_chunk_id),
405                )
406                .await?;
407
408                let last_raw_chunk =
409                    last_raw_chunk.ok_or(IndexeddbEventCacheStoreError::UnableToLoadChunk)?;
410                let max_chunk_id = max_chunk_id
411                    .map(|chunk| ChunkIdentifier::new(chunk.identifier))
412                    .ok_or(IndexeddbEventCacheStoreError::NoMaxChunkId)?;
413                let generator =
414                    ChunkIdentifierGenerator::new_from_previous_chunk_identifier(max_chunk_id);
415                Ok((Some(last_raw_chunk), generator))
416            }
417        }
418    }
419
420    #[instrument(skip(self))]
421    async fn load_previous_chunk(
422        &self,
423        linked_chunk_id: LinkedChunkId<'_>,
424        before_chunk_identifier: ChunkIdentifier,
425    ) -> Result<Option<RawChunk<Event, Gap>>, IndexeddbEventCacheStoreError> {
426        let _timer = timer!("method");
427
428        let transaction = self.transaction(
429            &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS],
430            IdbTransactionMode::Readonly,
431        )?;
432        if let Some(chunk) =
433            transaction.get_chunk_by_id(linked_chunk_id, before_chunk_identifier).await?
434            && let Some(previous_identifier) = chunk.previous
435        {
436            let previous_identifier = ChunkIdentifier::new(previous_identifier);
437            Ok(transaction.load_chunk_by_id(linked_chunk_id, previous_identifier).await?)
438        } else {
439            Ok(None)
440        }
441    }
442
443    #[instrument(skip(self))]
444    async fn load_thread_info(
445        &self,
446        room_id: &RoomId,
447        thread_id: &EventId,
448        insert_default_if_missing: bool,
449    ) -> Result<Option<ThreadInfo>, Self::Error> {
450        let _timer = timer!("method");
451
452        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readonly)?;
453
454        if let Some(thread) = transaction.load_thread_info(room_id, thread_id).await? {
455            return Ok(Some(thread.info));
456        } else if !insert_default_if_missing {
457            return Ok(None);
458        }
459
460        drop(transaction);
461
462        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
463
464        let thread = Thread {
465            room_id: room_id.to_owned(),
466            thread_id: thread_id.to_owned(),
467            info: ThreadInfo::new(),
468        };
469        transaction.update_thread_info(&thread)?;
470        transaction.commit().await?;
471
472        Ok(Some(thread.info))
473    }
474
475    #[instrument(skip(self))]
476    async fn update_thread_info(
477        &self,
478        room_id: &RoomId,
479        thread_id: &EventId,
480        thread_info: &ThreadInfo,
481    ) -> Result<(), Self::Error> {
482        let _timer = timer!("method");
483
484        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
485
486        let thread = Thread {
487            room_id: room_id.to_owned(),
488            thread_id: thread_id.to_owned(),
489            info: thread_info.clone(),
490        };
491        transaction.update_thread_info(&thread)?;
492        transaction.commit().await?;
493
494        Ok(())
495    }
496
497    #[instrument(skip(self))]
498    async fn clear_all_events(
499        &self,
500        room_id: Option<&RoomId>,
501    ) -> Result<(), IndexeddbEventCacheStoreError> {
502        let _timer = timer!("method");
503
504        let transaction = self.transaction(
505            &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS, keys::THREADS],
506            IdbTransactionMode::Readwrite,
507        )?;
508
509        match room_id {
510            // Clear all events.
511            None => {
512                transaction.clear::<types::Chunk>()?;
513                transaction.clear::<types::Event>()?;
514                transaction.clear::<types::Gap>()?;
515                transaction.commit().await?;
516            }
517
518            // Clear events for specific room.
519            Some(room_id) => {
520                // Delete linked chunks for the room and pinned-events caches.
521                {
522                    for linked_chunk_id in
523                        [LinkedChunkId::Room(room_id), LinkedChunkId::PinnedEvents(room_id)]
524                    {
525                        // Remove all the items, gaps and events about the
526                        // current `LinkedChunkId`.
527                        transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
528                        transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
529                        transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
530                    }
531                }
532
533                // Delete linked chunks for the thread caches.
534                {
535                    for thread in transaction.get_threads_by_room_id(room_id).await? {
536                        let linked_chunk_id = thread.linked_chunk();
537
538                        // Remove all the items, gaps and events about the
539                        // current `LinkedChunkId`.
540                        transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
541                        transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
542                        transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
543                    }
544                }
545
546                // Is everything alright? Good. We can commit the transaction.
547                transaction.commit().await?;
548            }
549        }
550        Ok(())
551    }
552
553    #[instrument(skip(self, events))]
554    async fn filter_duplicated_events(
555        &self,
556        linked_chunk_id: LinkedChunkId<'_>,
557        events: Vec<OwnedEventId>,
558    ) -> Result<Vec<(OwnedEventId, Position)>, IndexeddbEventCacheStoreError> {
559        let _timer = timer!("method");
560
561        if events.is_empty() {
562            return Ok(Vec::new());
563        }
564
565        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
566        let mut duplicated = Vec::new();
567        for event_id in events {
568            if let Some(types::Event::InBand(event)) =
569                transaction.get_event_by_id(linked_chunk_id, &event_id).await?
570            {
571                duplicated.push((event_id, event.position.into()));
572            }
573        }
574        Ok(duplicated)
575    }
576
577    #[instrument(skip(self, event_id))]
578    async fn find_event(
579        &self,
580        room_id: &RoomId,
581        event_id: &EventId,
582    ) -> Result<Option<Event>, IndexeddbEventCacheStoreError> {
583        let _timer = timer!("method");
584
585        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
586        transaction
587            .get_events_by_room(room_id, event_id)
588            .await
589            .map(|mut events| events.pop().map(Into::into))
590            .map_err(Into::into)
591    }
592
593    #[instrument(skip(self, event_id, filters))]
594    async fn find_event_relations(
595        &self,
596        room_id: &RoomId,
597        event_id: &EventId,
598        filters: Option<&[RelationType]>,
599    ) -> Result<Vec<(Event, Option<Position>)>, IndexeddbEventCacheStoreError> {
600        let _timer = timer!("method");
601
602        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
603
604        let mut related_events = HashMap::<OwnedEventId, types::Event>::new();
605        match filters {
606            Some(relation_types) if !relation_types.is_empty() => {
607                for relation_type in relation_types {
608                    let relation = (event_id, relation_type);
609                    let events = transaction.get_events_by_relation(room_id, relation).await?;
610                    for event in events {
611                        let Some(event_id) = event.event_id() else {
612                            return Err(IndexeddbEventCacheStoreError::EventWithoutId);
613                        };
614                        match event.linked_chunk_id() {
615                            LinkedChunkId::Room(_) => {
616                                // Prioritize events that come from a room
617                                // linked chunk
618                                related_events.insert(event_id.to_owned(), event);
619                            }
620                            _ => {
621                                // Remove position information from events that
622                                // come from any other type of linked chunk
623                                related_events
624                                    .entry(event_id.to_owned())
625                                    .or_insert_with(|| event.into_out_of_band_event());
626                            }
627                        }
628                    }
629                }
630            }
631            _ => {
632                for event in transaction.get_events_by_related_event(room_id, event_id).await? {
633                    let Some(event_id) = event.event_id() else {
634                        return Err(IndexeddbEventCacheStoreError::EventWithoutId);
635                    };
636                    match event.linked_chunk_id() {
637                        LinkedChunkId::Room(_) => {
638                            // Prioritize events that come from a room linked
639                            // chunk
640                            related_events.insert(event_id.to_owned(), event);
641                        }
642                        _ => {
643                            // Remove position information from events that come
644                            // from any other type of linked chunk
645                            related_events
646                                .entry(event_id.to_owned())
647                                .or_insert_with(|| event.into_out_of_band_event());
648                        }
649                    }
650                }
651            }
652        }
653        Ok(related_events
654            .into_values()
655            .map(|event| {
656                let position = event.position().map(Into::into);
657                (event.into(), position)
658            })
659            .collect())
660    }
661
662    #[instrument(skip(self))]
663    async fn find_events_before_timestamp(
664        &self,
665        room_id: &RoomId,
666        cutoff_ms: u64,
667    ) -> Result<Vec<Event>, IndexeddbEventCacheStoreError> {
668        let _timer = timer!("method");
669
670        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
671        let events = transaction.get_events_before_timestamp(room_id, cutoff_ms).await?;
672
673        let mut seen_event_ids = HashSet::new();
674        let mut results = Vec::new();
675        for event in events {
676            let Some(event_id) = event.event_id().map(ToOwned::to_owned) else {
677                continue;
678            };
679
680            if seen_event_ids.insert(event_id) {
681                results.push(Event::from(event));
682            }
683        }
684
685        Ok(results)
686    }
687
688    #[instrument(skip(self))]
689    async fn get_room_events(
690        &self,
691        room_id: &RoomId,
692        event_type: Option<&str>,
693        session_id: Option<&str>,
694    ) -> Result<Vec<Event>, IndexeddbEventCacheStoreError> {
695        let _timer = timer!("method");
696
697        // TODO: Make this more efficient so we don't load all events and filter
698        // them here. We should instead only load the relevant events.
699
700        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
701        transaction
702            .get_room_events(room_id)
703            .await
704            .map(|mut vec| {
705                vec.dedup_by(|a, b| {
706                    if let (Some(a), Some(b)) = (a.event_id(), b.event_id()) {
707                        a == b
708                    } else {
709                        false
710                    }
711                });
712                vec.into_iter()
713                    .map(Event::from)
714                    .filter(|e| {
715                        event_type.is_none_or(|event_type| {
716                            Some(event_type) == e.kind.event_type().as_deref()
717                        })
718                    })
719                    .filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
720                    .collect()
721            })
722            .map_err(Into::into)
723    }
724
725    #[instrument(skip(self, event))]
726    async fn save_event(
727        &self,
728        room_id: &RoomId,
729        event: Event,
730    ) -> Result<(), IndexeddbEventCacheStoreError> {
731        let _timer = timer!("method");
732
733        let Some(event_id) = event.event_id() else {
734            error!(%room_id, "Trying to save an event with no ID");
735            return Ok(());
736        };
737        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readwrite)?;
738
739        let mut events = transaction
740            .get_events_by_room(room_id, event_id)
741            .await?
742            .into_iter()
743            .map(|e| e.with_content(event.clone()))
744            .collect::<Vec<types::Event>>();
745        if events.is_empty() {
746            events.push(types::Event::OutOfBand(OutOfBandEvent {
747                linked_chunk_id: LinkedChunkId::Room(room_id).to_owned(),
748                content: event,
749                position: (),
750            }));
751        }
752        for event in events {
753            transaction.put_event(&event).await?;
754        }
755        transaction.commit().await?;
756        Ok(())
757    }
758
759    async fn optimize(&self) -> Result<(), Self::Error> {
760        Ok(())
761    }
762
763    async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
764        Ok(None)
765    }
766
767    async fn close(&self) -> Result<(), Self::Error> {
768        Ok(())
769    }
770
771    async fn reopen(&self) -> Result<(), Self::Error> {
772        Ok(())
773    }
774}
775
776#[cfg(all(test, target_family = "wasm"))]
777mod tests {
778    use matrix_sdk_base::{
779        event_cache::store::EventCacheStoreError, event_cache_store_integration_tests,
780        event_cache_store_integration_tests_time,
781    };
782    use uuid::Uuid;
783
784    use crate::{
785        event_cache_store::IndexeddbEventCacheStore, indexeddb_event_cache_store_integration_tests,
786    };
787
788    mod unencrypted {
789        use super::*;
790
791        wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
792
793        async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
794            let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
795            Ok(IndexeddbEventCacheStore::builder().database_name(name).build().await?)
796        }
797
798        event_cache_store_integration_tests!();
799        event_cache_store_integration_tests_time!();
800
801        indexeddb_event_cache_store_integration_tests!();
802    }
803
804    mod encrypted {
805        use std::sync::Arc;
806
807        use matrix_sdk_store_encryption::StoreCipher;
808
809        use super::*;
810
811        wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
812
813        async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
814            let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
815            Ok(IndexeddbEventCacheStore::builder()
816                .database_name(name)
817                .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
818                .build()
819                .await?)
820        }
821
822        event_cache_store_integration_tests!();
823        event_cache_store_integration_tests_time!();
824
825        indexeddb_event_cache_store_integration_tests!();
826    }
827}