diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs index 87b52e17f7a..8dc19cc2b06 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; -use matrix_sdk_base::event_cache::store::MemoryStore; +use matrix_sdk_base::event_cache::store::{media::MediaService, MemoryStore}; use matrix_sdk_store_encryption::StoreCipher; use web_sys::DomException; @@ -66,10 +66,11 @@ impl IndexeddbEventCacheStoreBuilder { /// and the provided store cipher. pub async fn build(self) -> Result { Ok(IndexeddbEventCacheStore { - inner: open_and_upgrade_db(&self.database_name).await?, + inner: Rc::new(open_and_upgrade_db(&self.database_name).await?), serializer: IndexeddbEventCacheStoreSerializer::new(IndexeddbSerializer::new( self.store_cipher, )), + media_service: MediaService::new(), memory_store: MemoryStore::new(), }) } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs index ffe6a12406a..020a546b3c6 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/integration_tests.rs @@ -684,3 +684,30 @@ macro_rules! indexeddb_event_cache_store_integration_tests { } }; } + +/// This is a partial copy of +/// [`matrix_sdk_base::event_cache_store_media_integration_tests`] that contains +/// tests for functions of [`EventCacheStoreMedia`] that are implemented by +/// [`IndexeddbEventCacheStore`]. +/// +/// This is useful for adding functionality to [`IndexeddbEventCacheStore`] over +/// multiple pull requests. Once a full implementation [`EventCacheStoreMedia`] +/// exists, this will be replaced with the actual integration tests referenced +/// above. +#[macro_export] +macro_rules! event_cache_store_media_integration_tests { + () => { + mod event_cache_store_media_integration_tests { + use matrix_sdk_base::event_cache::store::media::EventCacheStoreMediaIntegrationTests; + use matrix_sdk_test::async_test; + + use super::get_event_cache_store; + + #[async_test] + async fn test_store_media_retention_policy() { + let event_cache_store_media = get_event_cache_store().await.unwrap(); + event_cache_store_media.test_store_media_retention_policy().await; + } + } + }; +} diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs index ef5db3eb5bf..9db8da0ee20 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -109,6 +109,7 @@ pub mod v1 { pub mod keys { pub const CORE: &str = "core"; + pub const CORE_KEY_PATH: &str = "id"; pub const LEASES: &str = "leases"; pub const LEASES_KEY_PATH: &str = "id"; pub const ROOMS: &str = "rooms"; @@ -129,6 +130,7 @@ pub mod v1 { pub const EVENTS_RELATION_RELATION_TYPES: &str = "events_relation_relation_type"; pub const GAPS: &str = "gaps"; pub const GAPS_KEY_PATH: &str = "id"; + pub const MEDIA_RETENTION_POLICY_KEY: &str = "media_retention_policy"; } /// Create all object stores and indices for v1 database @@ -142,8 +144,12 @@ pub mod v1 { } /// Create an object store for tracking miscellaneous information + /// + /// * Primary Key - `id` fn create_core_object_store(db: &IdbDatabase) -> Result<(), DomException> { - let _ = db.create_object_store(keys::CORE)?; + let mut object_store_params = IdbObjectStoreParameters::new(); + object_store_params.key_path(Some(&keys::CORE_KEY_PATH.into())); + let _ = db.create_object_store_with_params(keys::CORE, &object_store_params)?; Ok(()) } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs index 72376cbcc82..05eee8d291f 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -14,13 +14,16 @@ #![allow(unused)] -use std::time::Duration; +use std::{rc::Rc, time::Duration}; use indexed_db_futures::IdbDatabase; use matrix_sdk_base::{ event_cache::{ store::{ - media::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, + media::{ + EventCacheStoreMedia, IgnoreMediaRetentionPolicy, MediaRetentionPolicy, + MediaService, + }, EventCacheStore, MemoryStore, }, Event, Gap, @@ -33,8 +36,8 @@ use matrix_sdk_base::{ timer, }; use ruma::{ - events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, - RoomId, + events::relation::RelationType, time::SystemTime, EventId, MilliSecondsSinceUnixEpoch, MxcUri, + OwnedEventId, RoomId, }; use tracing::{error, instrument, trace}; use web_sys::IdbTransactionMode; @@ -48,7 +51,7 @@ use crate::event_cache_store::{ mod builder; mod error; -#[cfg(test)] +#[cfg(all(test, target_family = "wasm"))] mod integration_tests; mod migrations; mod serializer; @@ -63,12 +66,15 @@ pub use error::IndexeddbEventCacheStoreError; /// contexts. /// /// [1]: matrix_sdk_base::event_cache::store::EventCacheStore -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IndexeddbEventCacheStore { // A handle to the IndexedDB database - inner: IdbDatabase, + inner: Rc, // A serializer with functionality tailored to `IndexeddbEventCacheStore` serializer: IndexeddbEventCacheStoreSerializer, + // A service for conveniently delegating media-related queries to an `EventCacheStoreMedia` + // implementation + media_service: MediaService, // An in-memory store for providing temporary implementations for // functions of `EventCacheStore`. // @@ -99,36 +105,11 @@ impl IndexeddbEventCacheStore { } } -// Small hack to have the following macro invocation act as the appropriate -// trait impl block on wasm, but still be compiled on non-wasm as a regular -// impl block otherwise. -// -// The trait impl doesn't compile on non-wasm due to unfulfilled trait bounds, -// this hack allows us to still have most of rust-analyzer's IDE functionality -// within the impl block without having to set it up to check things against -// the wasm target (which would disable many other parts of the codebase). -#[cfg(target_arch = "wasm32")] -macro_rules! impl_event_cache_store { - ( $($body:tt)* ) => { - #[async_trait::async_trait(?Send)] - impl EventCacheStore for IndexeddbEventCacheStore { - type Error = IndexeddbEventCacheStoreError; - - $($body)* - } - }; -} +#[cfg(target_family = "wasm")] +#[async_trait::async_trait(?Send)] +impl EventCacheStore for IndexeddbEventCacheStore { + type Error = IndexeddbEventCacheStoreError; -#[cfg(not(target_arch = "wasm32"))] -macro_rules! impl_event_cache_store { - ( $($body:tt)* ) => { - impl IndexeddbEventCacheStore { - $($body)* - } - }; -} - -impl_event_cache_store! { #[instrument(skip(self))] async fn try_take_leased_lock( &self, @@ -543,10 +524,7 @@ impl_event_cache_store! { ignore_policy: IgnoreMediaRetentionPolicy, ) -> Result<(), IndexeddbEventCacheStoreError> { let _timer = timer!("method"); - self.memory_store - .add_media_content(request, content, ignore_policy) - .await - .map_err(IndexeddbEventCacheStoreError::MemoryStore) + self.media_service.add_media_content(self, request, content, ignore_policy).await } #[instrument(skip_all)] @@ -568,10 +546,7 @@ impl_event_cache_store! { request: &MediaRequestParameters, ) -> Result>, IndexeddbEventCacheStoreError> { let _timer = timer!("method"); - self.memory_store - .get_media_content(request) - .await - .map_err(IndexeddbEventCacheStoreError::MemoryStore) + self.media_service.get_media_content(self, request).await } #[instrument(skip_all)] @@ -592,10 +567,7 @@ impl_event_cache_store! { uri: &MxcUri, ) -> Result>, IndexeddbEventCacheStoreError> { let _timer = timer!("method"); - self.memory_store - .get_media_content_for_uri(uri) - .await - .map_err(IndexeddbEventCacheStoreError::MemoryStore) + self.media_service.get_media_content_for_uri(self, uri).await } #[instrument(skip(self))] @@ -616,16 +588,13 @@ impl_event_cache_store! { policy: MediaRetentionPolicy, ) -> Result<(), IndexeddbEventCacheStoreError> { let _timer = timer!("method"); - self.memory_store - .set_media_retention_policy(policy) - .await - .map_err(IndexeddbEventCacheStoreError::MemoryStore) + self.media_service.set_media_retention_policy(self, policy).await } #[instrument(skip_all)] fn media_retention_policy(&self) -> MediaRetentionPolicy { let _timer = timer!("method"); - self.memory_store.media_retention_policy() + self.media_service.media_retention_policy() } #[instrument(skip_all)] @@ -633,25 +602,127 @@ impl_event_cache_store! { &self, request: &MediaRequestParameters, ignore_policy: IgnoreMediaRetentionPolicy, + ) -> Result<(), IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.media_service.set_ignore_media_retention_policy(self, request, ignore_policy).await + } + + #[instrument(skip_all)] + async fn clean_up_media_cache(&self) -> Result<(), IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.media_service.clean_up_media_cache(self).await + } +} + +#[cfg(target_family = "wasm")] +#[async_trait::async_trait(?Send)] +impl EventCacheStoreMedia for IndexeddbEventCacheStore { + type Error = IndexeddbEventCacheStoreError; + + #[instrument(skip_all)] + async fn media_retention_policy_inner( + &self, + ) -> Result, IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], IdbTransactionMode::Readonly)? + .get_media_retention_policy() + .await + .map_err(Into::into) + } + + #[instrument(skip_all)] + async fn set_media_retention_policy_inner( + &self, + policy: MediaRetentionPolicy, + ) -> Result<(), IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.transaction(&[MediaRetentionPolicy::OBJECT_STORE], IdbTransactionMode::Readwrite)? + .put_item(&policy) + .await + .map_err(Into::into) + } + + #[instrument(skip_all)] + async fn add_media_content_inner( + &self, + request: &MediaRequestParameters, + content: Vec, + current_time: SystemTime, + policy: MediaRetentionPolicy, + ignore_policy: IgnoreMediaRetentionPolicy, ) -> Result<(), IndexeddbEventCacheStoreError> { let _timer = timer!("method"); self.memory_store - .set_ignore_media_retention_policy(request, ignore_policy) + .add_media_content_inner(request, content, current_time, policy, ignore_policy) .await .map_err(IndexeddbEventCacheStoreError::MemoryStore) } #[instrument(skip_all)] - async fn clean_up_media_cache(&self) -> Result<(), IndexeddbEventCacheStoreError> { + async fn set_ignore_media_retention_policy_inner( + &self, + request: &MediaRequestParameters, + ignore_policy: IgnoreMediaRetentionPolicy, + ) -> Result<(), IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.memory_store + .set_ignore_media_retention_policy_inner(request, ignore_policy) + .await + .map_err(IndexeddbEventCacheStoreError::MemoryStore) + } + + #[instrument(skip_all)] + async fn get_media_content_inner( + &self, + request: &MediaRequestParameters, + current_time: SystemTime, + ) -> Result>, IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.memory_store + .get_media_content_inner(request, current_time) + .await + .map_err(IndexeddbEventCacheStoreError::MemoryStore) + } + + #[instrument(skip_all)] + async fn get_media_content_for_uri_inner( + &self, + uri: &MxcUri, + current_time: SystemTime, + ) -> Result>, IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.memory_store + .get_media_content_for_uri_inner(uri, current_time) + .await + .map_err(IndexeddbEventCacheStoreError::MemoryStore) + } + + #[instrument(skip_all)] + async fn clean_up_media_cache_inner( + &self, + policy: MediaRetentionPolicy, + current_time: SystemTime, + ) -> Result<(), IndexeddbEventCacheStoreError> { let _timer = timer!("method"); self.memory_store - .clean_up_media_cache() + .clean_up_media_cache_inner(policy, current_time) + .await + .map_err(IndexeddbEventCacheStoreError::MemoryStore) + } + + #[instrument(skip_all)] + async fn last_media_cleanup_time_inner( + &self, + ) -> Result, IndexeddbEventCacheStoreError> { + let _timer = timer!("method"); + self.memory_store + .last_media_cleanup_time_inner() .await .map_err(IndexeddbEventCacheStoreError::MemoryStore) } } -#[cfg(test)] +#[cfg(all(test, target_family = "wasm"))] mod tests { use matrix_sdk_base::{ event_cache::store::{EventCacheStore, EventCacheStoreError}, @@ -661,7 +732,8 @@ mod tests { use uuid::Uuid; use crate::{ - event_cache_store::IndexeddbEventCacheStore, indexeddb_event_cache_store_integration_tests, + event_cache_store::IndexeddbEventCacheStore, event_cache_store_media_integration_tests, + indexeddb_event_cache_store_integration_tests, }; mod unencrypted { @@ -674,14 +746,12 @@ mod tests { Ok(IndexeddbEventCacheStore::builder().database_name(name).build().await?) } - #[cfg(target_family = "wasm")] event_cache_store_integration_tests!(); + event_cache_store_integration_tests_time!(); - #[cfg(target_family = "wasm")] indexeddb_event_cache_store_integration_tests!(); - #[cfg(target_family = "wasm")] - event_cache_store_integration_tests_time!(); + event_cache_store_media_integration_tests!(); } mod encrypted { @@ -694,13 +764,11 @@ mod tests { Ok(IndexeddbEventCacheStore::builder().database_name(name).build().await?) } - #[cfg(target_family = "wasm")] event_cache_store_integration_tests!(); + event_cache_store_integration_tests_time!(); - #[cfg(target_family = "wasm")] indexeddb_event_cache_store_integration_tests!(); - #[cfg(target_family = "wasm")] - event_cache_store_integration_tests_time!(); + event_cache_store_media_integration_tests!(); } } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/mod.rs index 78817f6da78..d15cea801d8 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/mod.rs @@ -52,7 +52,7 @@ impl From for IndexeddbEventCacheStoreSerializerEr /// [`EventCacheStore`][1]. /// /// [1]: matrix_sdk_base::event_cache::store::EventCacheStore -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IndexeddbEventCacheStoreSerializer { inner: IndexeddbSerializer, } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs index fcb67a648a9..2af9b046e84 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs @@ -29,7 +29,10 @@ use std::sync::LazyLock; -use matrix_sdk_base::linked_chunk::{ChunkIdentifier, LinkedChunkId}; +use matrix_sdk_base::{ + event_cache::store::media::MediaRetentionPolicy, + linked_chunk::{ChunkIdentifier, LinkedChunkId}, +}; use matrix_sdk_crypto::CryptoStoreError; use ruma::{events::relation::RelationType, EventId, OwnedEventId, RoomId}; use serde::{Deserialize, Serialize}; @@ -231,6 +234,13 @@ impl From for IndexedKeyRange { } } +/// A representation of the primary key of the [`CORE`][1] object store. +/// The key may or may not be hashed depending on the +/// provided [`IndexeddbSerializer`]. +/// +/// [1]: crate::event_cache_store::migrations::v1::create_core_object_store +pub type IndexedCoreIdKey = String; + /// A (possibly) encrypted representation of a [`Lease`] pub type IndexedLeaseContent = MaybeEncrypted; @@ -268,6 +278,9 @@ pub type IndexedEventContent = MaybeEncrypted; /// A (possibly) encrypted representation of a [`Gap`] pub type IndexedGapContent = MaybeEncrypted; +/// A (possibly) encrypted representation of a [`MediaRetentionPolicy`] +pub type IndexedMediaRetentionPolicyContent = MaybeEncrypted; + /// Represents the [`LEASES`][1] object store. /// /// [1]: crate::event_cache_store::migrations::v1::create_lease_object_store @@ -291,7 +304,7 @@ impl Indexed for Lease { serializer: &IndexeddbSerializer, ) -> Result { Ok(IndexedLease { - id: IndexedLeaseIdKey::encode(&self.key, serializer), + id: >::encode(&self.key, serializer), content: serializer.maybe_encrypt_value(self)?, }) } @@ -826,3 +839,47 @@ impl<'a> IndexedPrefixKeyComponentBounds<'a, Gap, LinkedChunkId<'a>> for Indexed ) } } + +/// Represents the [`MediaRetentionPolicy`] record in the [`CORE`][1] object +/// store. +/// +/// [1]: crate::event_cache_store::migrations::v1::create_core_object_store +#[derive(Debug, Serialize, Deserialize)] +pub struct IndexedMediaRetentionPolicy { + /// The primary key of the object store. + pub id: IndexedCoreIdKey, + /// The (possibly) encrypted content - i.e., a [`MediaRetentionPolicy`]. + pub content: IndexedMediaRetentionPolicyContent, +} + +impl Indexed for MediaRetentionPolicy { + const OBJECT_STORE: &'static str = keys::CORE; + + type IndexedType = IndexedMediaRetentionPolicy; + type Error = CryptoStoreError; + + fn to_indexed( + &self, + serializer: &IndexeddbSerializer, + ) -> Result { + Ok(Self::IndexedType { + id: >::encode((), serializer), + content: serializer.maybe_encrypt_value(self)?, + }) + } + + fn from_indexed( + indexed: Self::IndexedType, + serializer: &IndexeddbSerializer, + ) -> Result { + serializer.maybe_decrypt_value(indexed.content) + } +} + +impl IndexedKey for IndexedCoreIdKey { + type KeyComponents<'a> = (); + + fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + serializer.encode_key_as_string(keys::CORE, keys::MEDIA_RETENTION_POLICY_KEY) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs index 760fb95fe83..3ea822f9d8d 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs @@ -14,7 +14,10 @@ use indexed_db_futures::{prelude::IdbTransaction, IdbQuerySource}; use matrix_sdk_base::{ - event_cache::{store::EventCacheStoreError, Event as RawEvent, Gap as RawGap}, + event_cache::{ + store::{media::MediaRetentionPolicy, EventCacheStoreError}, + Event as RawEvent, Gap as RawGap, + }, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, RawChunk}, }; use ruma::{events::relation::RelationType, EventId, OwnedEventId, RoomId}; @@ -27,15 +30,16 @@ use web_sys::IdbCursorDirection; use crate::event_cache_store::{ error::AsyncErrorDeps, + migrations::v1::keys, serializer::{ traits::{ Indexed, IndexedKey, IndexedKeyBounds, IndexedKeyComponentBounds, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds, }, types::{ - IndexedChunkIdKey, IndexedEventIdKey, IndexedEventPositionKey, IndexedEventRelationKey, - IndexedEventRoomKey, IndexedGapIdKey, IndexedKeyRange, IndexedLeaseIdKey, - IndexedNextChunkIdKey, + IndexedChunkIdKey, IndexedCoreIdKey, IndexedEventIdKey, IndexedEventPositionKey, + IndexedEventRelationKey, IndexedEventRoomKey, IndexedGapIdKey, IndexedKeyRange, + IndexedLeaseIdKey, IndexedNextChunkIdKey, }, IndexeddbEventCacheStoreSerializer, }, @@ -861,4 +865,11 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { self.delete_items_by_linked_chunk_id::(linked_chunk_id).await } + + /// Query IndexedDB for the stored [`MediaRetentionPolicy`] + pub async fn get_media_retention_policy( + &self, + ) -> Result, IndexeddbEventCacheStoreTransactionError> { + self.get_item_by_key_components::(()).await + } } diff --git a/crates/matrix-sdk-indexeddb/src/serializer.rs b/crates/matrix-sdk-indexeddb/src/serializer.rs index c6b5040554a..cb29b1ee77d 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer.rs @@ -35,6 +35,7 @@ const BASE64: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, general_ /// Handles the functionality of serializing and encrypting data for the /// indexeddb store. +#[derive(Clone)] pub struct IndexeddbSerializer { store_cipher: Option>, }