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