1#![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#[derive(Debug, Clone)]
67pub struct IndexeddbEventCacheStore {
68 inner: Rc<Database>,
70 serializer: IndexedTypeSerializer,
72}
73
74impl IndexeddbEventCacheStore {
75 pub fn builder() -> IndexeddbEventCacheStoreBuilder {
78 IndexeddbEventCacheStoreBuilder::default()
79 }
80
81 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 lease.expiration = expiration;
123
124 Some(lease)
125 } else {
126 if lease.expiration < now {
128 lease.holder = holder.to_owned();
130 lease.expiration = expiration;
131 lease.generation += 1;
132
133 Some(lease)
134 } else {
135 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 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 }
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 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 match transaction.get_chunk_by_next_chunk_id(linked_chunk_id, None).await {
378 Err(TransactionError::ItemIsNotUnique) => {
379 Err(IndexeddbEventCacheStoreError::ChunksContainDisjointLists)
383 }
384 Err(e) => {
385 Err(e.into())
388 }
389 Ok(None) => {
390 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 None => {
508 transaction.clear::<types::Chunk>()?;
509 transaction.clear::<types::Event>()?;
510 transaction.clear::<types::Gap>()?;
511 transaction.commit().await?;
512 }
513
514 Some(room_id) => {
516 {
518 for linked_chunk_id in
519 [LinkedChunkId::Room(room_id), LinkedChunkId::PinnedEvents(room_id)]
520 {
521 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 {
531 for thread in transaction.get_threads_by_room_id(room_id).await? {
532 let linked_chunk_id = thread.linked_chunk();
533
534 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 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 related_events.insert(event_id.to_owned(), event);
615 }
616 _ => {
617 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 related_events.insert(event_id.to_owned(), event);
637 }
638 _ => {
639 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 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}