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    ) -> Result<ThreadInfo, Self::Error> {
445        let _timer = timer!("method");
446
447        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readonly)?;
448
449        if let Some(thread) = transaction.load_thread_info(room_id, thread_id).await? {
450            return Ok(thread.info);
451        }
452
453        drop(transaction);
454
455        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
456
457        let thread = Thread {
458            room_id: room_id.to_owned(),
459            thread_id: thread_id.to_owned(),
460            info: ThreadInfo::new(),
461        };
462        transaction.update_thread_info(&thread)?;
463        transaction.commit().await?;
464
465        Ok(thread.info)
466    }
467
468    #[instrument(skip(self))]
469    async fn update_thread_info(
470        &self,
471        room_id: &RoomId,
472        thread_id: &EventId,
473        thread_info: &ThreadInfo,
474    ) -> Result<(), Self::Error> {
475        let _timer = timer!("method");
476
477        let transaction = self.transaction(&[keys::THREADS], IdbTransactionMode::Readwrite)?;
478
479        let thread = Thread {
480            room_id: room_id.to_owned(),
481            thread_id: thread_id.to_owned(),
482            info: thread_info.clone(),
483        };
484        transaction.update_thread_info(&thread)?;
485        transaction.commit().await?;
486
487        Ok(())
488    }
489
490    #[instrument(skip(self))]
491    async fn clear_all_events(
492        &self,
493        room_id: Option<&RoomId>,
494    ) -> Result<(), IndexeddbEventCacheStoreError> {
495        let _timer = timer!("method");
496
497        let transaction = self.transaction(
498            &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS, keys::THREADS],
499            IdbTransactionMode::Readwrite,
500        )?;
501
502        match room_id {
503            // Clear all events.
504            None => {
505                transaction.clear::<types::Chunk>()?;
506                transaction.clear::<types::Event>()?;
507                transaction.clear::<types::Gap>()?;
508                transaction.commit().await?;
509            }
510
511            // Clear events for specific room.
512            Some(room_id) => {
513                // Delete linked chunks for the room and pinned-events caches.
514                {
515                    for linked_chunk_id in
516                        [LinkedChunkId::Room(room_id), LinkedChunkId::PinnedEvents(room_id)]
517                    {
518                        // Remove all the items, gaps and events about the
519                        // current `LinkedChunkId`.
520                        transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
521                        transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
522                        transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
523                    }
524                }
525
526                // Delete linked chunks for the thread caches.
527                {
528                    for thread in transaction.get_threads_by_room_id(room_id).await? {
529                        let linked_chunk_id = thread.linked_chunk();
530
531                        // Remove all the items, gaps and events about the
532                        // current `LinkedChunkId`.
533                        transaction.delete_chunks_by_linked_chunk_id(linked_chunk_id).await?;
534                        transaction.delete_gaps_by_linked_chunk_id(linked_chunk_id).await?;
535                        transaction.delete_events_by_linked_chunk_id(linked_chunk_id).await?;
536                    }
537                }
538
539                // Is everything alright? Good. We can commit the transaction.
540                transaction.commit().await?;
541            }
542        }
543        Ok(())
544    }
545
546    #[instrument(skip(self, events))]
547    async fn filter_duplicated_events(
548        &self,
549        linked_chunk_id: LinkedChunkId<'_>,
550        events: Vec<OwnedEventId>,
551    ) -> Result<Vec<(OwnedEventId, Position)>, IndexeddbEventCacheStoreError> {
552        let _timer = timer!("method");
553
554        if events.is_empty() {
555            return Ok(Vec::new());
556        }
557
558        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
559        let mut duplicated = Vec::new();
560        for event_id in events {
561            if let Some(types::Event::InBand(event)) =
562                transaction.get_event_by_id(linked_chunk_id, &event_id).await?
563            {
564                duplicated.push((event_id, event.position.into()));
565            }
566        }
567        Ok(duplicated)
568    }
569
570    #[instrument(skip(self, event_id))]
571    async fn find_event(
572        &self,
573        room_id: &RoomId,
574        event_id: &EventId,
575    ) -> Result<Option<Event>, IndexeddbEventCacheStoreError> {
576        let _timer = timer!("method");
577
578        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
579        transaction
580            .get_events_by_room(room_id, event_id)
581            .await
582            .map(|mut events| events.pop().map(Into::into))
583            .map_err(Into::into)
584    }
585
586    #[instrument(skip(self, event_id, filters))]
587    async fn find_event_relations(
588        &self,
589        room_id: &RoomId,
590        event_id: &EventId,
591        filters: Option<&[RelationType]>,
592    ) -> Result<Vec<(Event, Option<Position>)>, IndexeddbEventCacheStoreError> {
593        let _timer = timer!("method");
594
595        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
596
597        let mut related_events = HashMap::<OwnedEventId, types::Event>::new();
598        match filters {
599            Some(relation_types) if !relation_types.is_empty() => {
600                for relation_type in relation_types {
601                    let relation = (event_id, relation_type);
602                    let events = transaction.get_events_by_relation(room_id, relation).await?;
603                    for event in events {
604                        let Some(event_id) = event.event_id() else {
605                            return Err(IndexeddbEventCacheStoreError::EventWithoutId);
606                        };
607                        match event.linked_chunk_id() {
608                            LinkedChunkId::Room(_) => {
609                                // Prioritize events that come from a room
610                                // linked chunk
611                                related_events.insert(event_id.to_owned(), event);
612                            }
613                            _ => {
614                                // Remove position information from events that
615                                // come from any other type of linked chunk
616                                related_events
617                                    .entry(event_id.to_owned())
618                                    .or_insert_with(|| event.into_out_of_band_event());
619                            }
620                        }
621                    }
622                }
623            }
624            _ => {
625                for event in transaction.get_events_by_related_event(room_id, event_id).await? {
626                    let Some(event_id) = event.event_id() else {
627                        return Err(IndexeddbEventCacheStoreError::EventWithoutId);
628                    };
629                    match event.linked_chunk_id() {
630                        LinkedChunkId::Room(_) => {
631                            // Prioritize events that come from a room linked
632                            // chunk
633                            related_events.insert(event_id.to_owned(), event);
634                        }
635                        _ => {
636                            // Remove position information from events that come
637                            // from any other type of linked chunk
638                            related_events
639                                .entry(event_id.to_owned())
640                                .or_insert_with(|| event.into_out_of_band_event());
641                        }
642                    }
643                }
644            }
645        }
646        Ok(related_events
647            .into_values()
648            .map(|event| {
649                let position = event.position().map(Into::into);
650                (event.into(), position)
651            })
652            .collect())
653    }
654
655    #[instrument(skip(self))]
656    async fn get_room_events(
657        &self,
658        room_id: &RoomId,
659        event_type: Option<&str>,
660        session_id: Option<&str>,
661    ) -> Result<Vec<Event>, IndexeddbEventCacheStoreError> {
662        let _timer = timer!("method");
663
664        // TODO: Make this more efficient so we don't load all events and filter
665        // them here. We should instead only load the relevant events.
666
667        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readonly)?;
668        transaction
669            .get_room_events(room_id)
670            .await
671            .map(|mut vec| {
672                vec.dedup_by(|a, b| {
673                    if let (Some(a), Some(b)) = (a.event_id(), b.event_id()) {
674                        a == b
675                    } else {
676                        false
677                    }
678                });
679                vec.into_iter()
680                    .map(Event::from)
681                    .filter(|e| {
682                        event_type.is_none_or(|event_type| {
683                            Some(event_type) == e.kind.event_type().as_deref()
684                        })
685                    })
686                    .filter(|e| session_id.is_none_or(|s| Some(s) == e.kind.session_id()))
687                    .collect()
688            })
689            .map_err(Into::into)
690    }
691
692    #[instrument(skip(self, event))]
693    async fn save_event(
694        &self,
695        room_id: &RoomId,
696        event: Event,
697    ) -> Result<(), IndexeddbEventCacheStoreError> {
698        let _timer = timer!("method");
699
700        let Some(event_id) = event.event_id() else {
701            error!(%room_id, "Trying to save an event with no ID");
702            return Ok(());
703        };
704        let transaction = self.transaction(&[keys::EVENTS], IdbTransactionMode::Readwrite)?;
705
706        let mut events = transaction
707            .get_events_by_room(room_id, event_id)
708            .await?
709            .into_iter()
710            .map(|e| e.with_content(event.clone()))
711            .collect::<Vec<types::Event>>();
712        if events.is_empty() {
713            events.push(types::Event::OutOfBand(OutOfBandEvent {
714                linked_chunk_id: LinkedChunkId::Room(room_id).to_owned(),
715                content: event,
716                position: (),
717            }));
718        }
719        for event in events {
720            transaction.put_event(&event).await?;
721        }
722        transaction.commit().await?;
723        Ok(())
724    }
725
726    async fn optimize(&self) -> Result<(), Self::Error> {
727        Ok(())
728    }
729
730    async fn get_size(&self) -> Result<Option<usize>, Self::Error> {
731        Ok(None)
732    }
733
734    async fn close(&self) -> Result<(), Self::Error> {
735        Ok(())
736    }
737
738    async fn reopen(&self) -> Result<(), Self::Error> {
739        Ok(())
740    }
741}
742
743#[cfg(all(test, target_family = "wasm"))]
744mod tests {
745    use matrix_sdk_base::{
746        event_cache::store::EventCacheStoreError, event_cache_store_integration_tests,
747        event_cache_store_integration_tests_time,
748    };
749    use uuid::Uuid;
750
751    use crate::{
752        event_cache_store::IndexeddbEventCacheStore, indexeddb_event_cache_store_integration_tests,
753    };
754
755    mod unencrypted {
756        use super::*;
757
758        wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
759
760        async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
761            let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
762            Ok(IndexeddbEventCacheStore::builder().database_name(name).build().await?)
763        }
764
765        event_cache_store_integration_tests!();
766        event_cache_store_integration_tests_time!();
767
768        indexeddb_event_cache_store_integration_tests!();
769    }
770
771    mod encrypted {
772        use std::sync::Arc;
773
774        use matrix_sdk_store_encryption::StoreCipher;
775
776        use super::*;
777
778        wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
779
780        async fn get_event_cache_store() -> Result<IndexeddbEventCacheStore, EventCacheStoreError> {
781            let name = format!("test-event-cache-store-{}", Uuid::new_v4().as_hyphenated());
782            Ok(IndexeddbEventCacheStore::builder()
783                .database_name(name)
784                .store_cipher(Arc::new(StoreCipher::new().expect("store cipher")))
785                .build()
786                .await?)
787        }
788
789        event_cache_store_integration_tests!();
790        event_cache_store_integration_tests_time!();
791
792        indexeddb_event_cache_store_integration_tests!();
793    }
794}