diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index e3173d8b9aa..5b72ec48aa0 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -20,6 +20,9 @@ media-store = ["dep:matrix-sdk-base"] state-store = ["dep:matrix-sdk-base", "growable-bloom-filter"] e2e-encryption = ["dep:matrix-sdk-crypto"] testing = ["matrix-sdk-crypto?/testing"] +experimental-encrypted-state-events = [ + "matrix-sdk-crypto?/experimental-encrypted-state-events" +] [dependencies] anyhow.workspace = true diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index 440125fb5ae..e88c29ab58f 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -18,7 +18,7 @@ use indexed_db_futures::{prelude::*, web_sys::DomException}; use tracing::info; use wasm_bindgen::JsValue; -use crate::{crypto_store::Result, serializer::IndexeddbSerializer, IndexeddbCryptoStoreError}; +use crate::{crypto_store::Result, serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError}; mod old_keys; mod v0_to_v5; @@ -100,7 +100,7 @@ const MAX_SUPPORTED_SCHEMA_VERSION: u32 = 99; /// of the schema if necessary. pub async fn open_and_upgrade_db( name: &str, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { // Move the DB version up from where it is to the latest version. // @@ -282,7 +282,7 @@ mod tests { #[async_test] async fn test_count_lots_of_sessions_v8() { let cipher = Arc::new(StoreCipher::new().unwrap()); - let serializer = IndexeddbSerializer::new(Some(cipher.clone())); + let serializer = SafeEncodeSerializer::new(Some(cipher.clone())); // Session keys are slow to create, so make one upfront and use it for every // session let session_key = create_session_key(); @@ -319,7 +319,7 @@ mod tests { /// Make lots of sessions and see how long it takes to count them in v10 #[async_test] async fn test_count_lots_of_sessions_v10() { - let serializer = IndexeddbSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); + let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); // Session keys are slow to create, so make one upfront and use it for every // session @@ -395,7 +395,7 @@ mod tests { i: usize, session_key: &SessionKey, cipher: &Arc, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> (JsValue, JsValue) { let session = create_inbound_group_session(i, session_key); let pickled_session = session.pickle().await; @@ -416,7 +416,7 @@ mod tests { async fn create_inbound_group_sessions3_record( i: usize, session_key: &SessionKey, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> (JsValue, JsValue) { let session = create_inbound_group_session(i, session_key); let pickled_session = session.pickle().await; @@ -682,7 +682,7 @@ mod tests { // entry. let db = create_v5_db(&db_name).await.unwrap(); - let serializer = IndexeddbSerializer::new(store_cipher.clone()); + let serializer = SafeEncodeSerializer::new(store_cipher.clone()); let txn = db .transaction_on_one_with_mode( diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs index 2e6c1bb2297..201f9d824b1 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v10_to_v11.rs @@ -24,14 +24,14 @@ use crate::{ keys, migrations::{do_schema_upgrade, old_keys, MigrationDb}, }, - serializer::IndexeddbSerializer, + serializer::SafeEncodeSerializer, }; /// Migrate data from `backup_keys.backup_key_v1` to /// `backup_keys.backup_version_v1`. pub(crate) async fn data_migrate( name: &str, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> crate::crypto_store::Result<()> { let db = MigrationDb::new(name, 11).await?; let txn = db.transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readwrite)?; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs index 09822391a02..f544cb36600 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v13_to_v14.rs @@ -19,10 +19,10 @@ use web_sys::{DomException, IdbTransactionMode}; use super::MigrationDb; use crate::{ crypto_store::{keys, migrations::do_schema_upgrade, Result}, - serializer::IndexeddbSerializer, + serializer::SafeEncodeSerializer, }; -pub(crate) async fn data_migrate(name: &str, _: &IndexeddbSerializer) -> Result<()> { +pub(crate) async fn data_migrate(name: &str, _: &SafeEncodeSerializer) -> Result<()> { let db = MigrationDb::new(name, 14).await?; let transaction = db.transaction_on_one_with_mode( keys::RECEIVED_ROOM_KEY_BUNDLES, diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs index dd54665f6a2..bd36367b8d6 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v5_to_v7.rs @@ -30,7 +30,7 @@ use crate::{ migrations::{add_nonunique_index, do_schema_upgrade, old_keys, v7, MigrationDb}, Result, }, - serializer::IndexeddbSerializer, + serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError, }; @@ -51,7 +51,7 @@ pub(crate) async fn schema_add(name: &str) -> Result<(), DomException> { } /// Migrate data from `inbound_group_sessions` into `inbound_group_sessions2`. -pub(crate) async fn data_migrate(name: &str, serializer: &IndexeddbSerializer) -> Result<()> { +pub(crate) async fn data_migrate(name: &str, serializer: &SafeEncodeSerializer) -> Result<()> { let db = MigrationDb::new(name, 7).await?; // The new store has been made for inbound group sessions; time to populate it. diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7.rs index 05631e79870..3533a9992f5 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7.rs @@ -34,7 +34,7 @@ pub struct InboundGroupSessionIndexedDbObject2 { #[serde( default, skip_serializing_if = "std::ops::Not::not", - with = "crate::serialize_bool_for_indexeddb" + with = "crate::serializer::foreign::bool" )] pub needs_backup: bool, } diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs index 8df6a3d50bc..18675126767 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v7_to_v8.rs @@ -25,7 +25,7 @@ use crate::{ migrations::{do_schema_upgrade, old_keys, v7, MigrationDb}, Result, }, - serializer::IndexeddbSerializer, + serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError, }; @@ -33,7 +33,7 @@ use crate::{ /// `inbound_group_sessions` verbatim into `inbound_group_sessions2`. What we /// should have done is re-hash them using the new table name, so we fix them up /// here. -pub(crate) async fn data_migrate(name: &str, serializer: &IndexeddbSerializer) -> Result<()> { +pub(crate) async fn data_migrate(name: &str, serializer: &SafeEncodeSerializer) -> Result<()> { let db = MigrationDb::new(name, 8).await?; let txn = db.transaction_on_one_with_mode( diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs index 61a8f58da3e..cfb86aa5860 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs @@ -29,7 +29,7 @@ use crate::{ }, InboundGroupSessionIndexedDbObject, Result, }, - serializer::IndexeddbSerializer, + serializer::SafeEncodeSerializer, IndexeddbCryptoStoreError, }; @@ -59,7 +59,7 @@ pub(crate) async fn schema_add(name: &str) -> Result<(), DomException> { } /// Migrate data from `inbound_group_sessions2` into `inbound_group_sessions3`. -pub(crate) async fn data_migrate(name: &str, serializer: &IndexeddbSerializer) -> Result<()> { +pub(crate) async fn data_migrate(name: &str, serializer: &SafeEncodeSerializer) -> Result<()> { let db = MigrationDb::new(name, 10).await?; let txn = db.transaction_on_multi_with_mode( diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 2eeecd557df..b79a7eba1a1 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -52,7 +52,7 @@ use web_sys::IdbKeyRange; use crate::{ crypto_store::migrations::open_and_upgrade_db, - serializer::{IndexeddbSerializer, IndexeddbSerializerError, MaybeEncrypted}, + serializer::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError}, }; mod migrations; @@ -122,7 +122,7 @@ pub struct IndexeddbCryptoStore { name: String, pub(crate) inner: IdbDatabase, - serializer: IndexeddbSerializer, + serializer: SafeEncodeSerializer, save_changes_lock: Arc>, } @@ -155,14 +155,14 @@ pub enum IndexeddbCryptoStoreError { SchemaTooNewError { max_supported_version: u32, current_version: u32 }, } -impl From for IndexeddbCryptoStoreError { - fn from(value: IndexeddbSerializerError) -> Self { +impl From for IndexeddbCryptoStoreError { + fn from(value: SafeEncodeSerializerError) -> Self { match value { - IndexeddbSerializerError::Serialization(error) => Self::Serialization(error), - IndexeddbSerializerError::DomException { code, name, message } => { + SafeEncodeSerializerError::Serialization(error) => Self::Serialization(error), + SafeEncodeSerializerError::DomException { code, name, message } => { Self::DomException { code, name, message } } - IndexeddbSerializerError::CryptoStoreError(crypto_store_error) => { + SafeEncodeSerializerError::CryptoStoreError(crypto_store_error) => { Self::CryptoStoreError(crypto_store_error) } } @@ -287,7 +287,7 @@ impl IndexeddbCryptoStore { ) -> Result { let name = format!("{prefix:0}::matrix-sdk-crypto"); - let serializer = IndexeddbSerializer::new(store_cipher); + let serializer = SafeEncodeSerializer::new(store_cipher); debug!("IndexedDbCryptoStore: opening main store {name}"); let db = open_and_upgrade_db(&name, &serializer).await?; @@ -1733,7 +1733,7 @@ struct GossipRequestIndexedDbObject { #[serde( default, skip_serializing_if = "std::ops::Not::not", - with = "crate::serialize_bool_for_indexeddb" + with = "crate::serializer::foreign::bool" )] unsent: bool, } @@ -1766,7 +1766,7 @@ struct InboundGroupSessionIndexedDbObject { #[serde( default, skip_serializing_if = "std::ops::Not::not", - with = "crate::serialize_bool_for_indexeddb" + with = "crate::serializer::foreign::bool" )] needs_backup: bool, @@ -1804,7 +1804,7 @@ impl InboundGroupSessionIndexedDbObject { /// session. pub async fn from_session( session: &InboundGroupSession, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { let session_id = serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id()); @@ -1837,7 +1837,7 @@ mod unit_tests { use ruma::{device_id, room_id, user_id}; use super::InboundGroupSessionIndexedDbObject; - use crate::serializer::{IndexeddbSerializer, MaybeEncrypted}; + use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer}; #[test] fn needs_backup_is_serialized_as_a_u8_in_json() { @@ -1915,7 +1915,7 @@ mod unit_tests { ) .unwrap(); - InboundGroupSessionIndexedDbObject::from_session(&session, &IndexeddbSerializer::new(None)) + InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None)) .await .unwrap() } diff --git a/crates/matrix-sdk-indexeddb/src/error.rs b/crates/matrix-sdk-indexeddb/src/error.rs new file mode 100644 index 00000000000..b298f10d3ac --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/error.rs @@ -0,0 +1,23 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +use matrix_sdk_base::{SendOutsideWasm, SyncOutsideWasm}; + +/// A trait that combines the necessary traits needed for asynchronous runtimes, +/// but excludes them when running in a web environment - i.e., when +/// `#[cfg(target_family = "wasm")]`. +pub trait AsyncErrorDeps: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static {} + +impl AsyncErrorDeps for T where T: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static +{} 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 6d3ab624d74..3e989c350fc 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/builder.rs @@ -12,6 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License +// At the moment, this builder is not public outside of the crate, so we +// get a few dead code warnings; however, this will eventually be a public +// type, at which point the line below can be removed. +#![allow(dead_code)] + use std::{rc::Rc, sync::Arc}; use matrix_sdk_store_encryption::StoreCipher; @@ -19,9 +24,9 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ event_cache_store::{ error::IndexeddbEventCacheStoreError, migrations::open_and_upgrade_db, - serializer::IndexeddbEventCacheStoreSerializer, IndexeddbEventCacheStore, + IndexeddbEventCacheStore, }, - serializer::IndexeddbSerializer, + serializer::{IndexedTypeSerializer, SafeEncodeSerializer}, }; /// A type for conveniently building an [`IndexeddbEventCacheStore`] @@ -65,9 +70,7 @@ impl IndexeddbEventCacheStoreBuilder { pub async fn build(self) -> Result { Ok(IndexeddbEventCacheStore { inner: Rc::new(open_and_upgrade_db(&self.database_name).await?), - serializer: IndexeddbEventCacheStoreSerializer::new(IndexeddbSerializer::new( - self.store_cipher, - )), + serializer: IndexedTypeSerializer::new(SafeEncodeSerializer::new(self.store_cipher)), }) } } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/error.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/error.rs index 8baaaca2883..038294d2637 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/error.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/error.rs @@ -12,18 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License -use matrix_sdk_base::{event_cache::store::EventCacheStoreError, SendOutsideWasm, SyncOutsideWasm}; +use matrix_sdk_base::event_cache::store::EventCacheStoreError; +use serde::de::Error; use thiserror::Error; -use crate::event_cache_store::transaction::IndexeddbEventCacheStoreTransactionError; - -/// A trait that combines the necessary traits needed for asynchronous runtimes, -/// but excludes them when running in a web environment - i.e., when -/// `#[cfg(target_family = "wasm")]`. -pub trait AsyncErrorDeps: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static {} - -impl AsyncErrorDeps for T where T: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static -{} +use crate::transaction::TransactionError; #[derive(Debug, Error)] pub enum IndexeddbEventCacheStoreError { @@ -38,7 +31,7 @@ pub enum IndexeddbEventCacheStoreError { #[error("no max chunk id")] NoMaxChunkId, #[error("transaction: {0}")] - Transaction(#[from] IndexeddbEventCacheStoreTransactionError), + Transaction(#[from] TransactionError), } impl From for IndexeddbEventCacheStoreError { @@ -65,3 +58,15 @@ impl From for EventCacheStoreError { } } } + +impl From for EventCacheStoreError { + fn from(value: TransactionError) -> Self { + use TransactionError::*; + + match value { + DomException { .. } => Self::InvalidData { details: value.to_string() }, + Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())), + ItemIsNotUnique | ItemNotFound => Self::InvalidData { details: value.to_string() }, + } + } +} 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..02781b0e5ef 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 @@ -26,9 +26,9 @@ use matrix_sdk_base::{ use matrix_sdk_test::DEFAULT_TEST_ROOM_ID; use ruma::room_id; -use crate::event_cache_store::{ - transaction::IndexeddbEventCacheStoreTransactionError, IndexeddbEventCacheStore, - IndexeddbEventCacheStoreError, +use crate::{ + event_cache_store::{IndexeddbEventCacheStore, IndexeddbEventCacheStoreError}, + transaction::TransactionError, }; pub async fn test_linked_chunk_new_items_chunk(store: IndexeddbEventCacheStore) { @@ -423,9 +423,7 @@ pub async fn test_linked_chunk_update_is_a_transaction(store: IndexeddbEventCach // The operation fails with a constraint violation error. assert_matches!( err, - IndexeddbEventCacheStoreError::Transaction( - IndexeddbEventCacheStoreTransactionError::DomException { .. } - ) + IndexeddbEventCacheStoreError::Transaction(TransactionError::DomException { .. }) ); // If the updates have been handled transactionally, then no new chunks should 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 9978ea906fe..451b2f81d51 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -108,8 +108,6 @@ pub mod v1 { use super::*; 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"; @@ -134,7 +132,6 @@ pub mod v1 { /// Create all object stores and indices for v1 database pub fn create_object_stores(db: &IdbDatabase) -> Result<(), DomException> { - create_core_object_store(db)?; create_lease_object_store(db)?; create_linked_chunks_object_store(db)?; create_events_object_store(db)?; @@ -142,16 +139,6 @@ pub mod v1 { Ok(()) } - /// Create an object store for tracking miscellaneous information - /// - /// * Primary Key - `id` - fn create_core_object_store(db: &IdbDatabase) -> Result<(), DomException> { - 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(()) - } - /// Create an object store tracking leases on time-based locks fn create_lease_object_store(db: &IdbDatabase) -> Result<(), DomException> { let mut object_store_params = IdbObjectStoreParameters::new(); @@ -193,7 +180,7 @@ pub mod v1 { keys::EVENTS_ROOM, &keys::EVENTS_ROOM_KEY_PATH.into(), &events_room_params, - ); + )?; let events_position_params = IdbIndexParameters::new(); events_position_params.set_unique(true); 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 f7979a5b9f1..44ba91763fb 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -31,11 +31,14 @@ use ruma::{ use tracing::{error, instrument, trace}; use web_sys::IdbTransactionMode; -use crate::event_cache_store::{ - migrations::current::keys, - serializer::{traits::Indexed, IndexeddbEventCacheStoreSerializer}, - transaction::{IndexeddbEventCacheStoreTransaction, IndexeddbEventCacheStoreTransactionError}, - types::{ChunkType, InBandEvent, Lease, OutOfBandEvent}, +use crate::{ + event_cache_store::{ + migrations::current::keys, + transaction::IndexeddbEventCacheStoreTransaction, + types::{ChunkType, InBandEvent, Lease, OutOfBandEvent}, + }, + serializer::{Indexed, IndexedTypeSerializer}, + transaction::TransactionError, }; mod builder; @@ -60,7 +63,7 @@ pub struct IndexeddbEventCacheStore { // A handle to the IndexedDB database inner: Rc, // A serializer with functionality tailored to `IndexeddbEventCacheStore` - serializer: IndexeddbEventCacheStoreSerializer, + serializer: IndexedTypeSerializer, } impl IndexeddbEventCacheStore { @@ -309,7 +312,6 @@ impl EventCacheStore for IndexeddbEventCacheStore { > { let _timer = timer!("method"); - let owned_linked_chunk_id = linked_chunk_id.to_owned(); let transaction = self.transaction( &[keys::LINKED_CHUNKS, keys::EVENTS, keys::GAPS], IdbTransactionMode::Readonly, @@ -322,7 +324,7 @@ impl EventCacheStore for IndexeddbEventCacheStore { // for the last chunk in the room by getting the chunk which does not // have a next chunk. match transaction.get_chunk_by_next_chunk_id(linked_chunk_id, None).await { - Err(IndexeddbEventCacheStoreTransactionError::ItemIsNotUnique) => { + Err(TransactionError::ItemIsNotUnique) => { // If there are multiple chunks that do not have a next chunk, that // means we have more than one last chunk, which means that we have // more than one list in the room. @@ -514,10 +516,9 @@ impl EventCacheStore for IndexeddbEventCacheStore { #[cfg(all(test, target_family = "wasm"))] mod tests { use matrix_sdk_base::{ - event_cache::store::{EventCacheStore, EventCacheStoreError}, - event_cache_store_integration_tests, event_cache_store_integration_tests_time, + event_cache::store::EventCacheStoreError, event_cache_store_integration_tests, + event_cache_store_integration_tests_time, }; - use matrix_sdk_test::async_test; use uuid::Uuid; use crate::{ diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/constants.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/constants.rs new file mode 100644 index 00000000000..86c743268c5 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/constants.rs @@ -0,0 +1,94 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +use std::sync::LazyLock; + +use matrix_sdk_base::linked_chunk::ChunkIdentifier; +use ruma::OwnedEventId; + +use crate::{ + event_cache_store::types::Position, + serializer::{INDEXED_KEY_LOWER_CHARACTER, INDEXED_KEY_UPPER_CHARACTER}, +}; + +/// A [`ChunkIdentifier`] constructed with `0`. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`ChunkIdentifier`]s when used in conjunction with +/// [`INDEXED_KEY_UPPER_CHUNK_IDENTIFIER`]. +pub static INDEXED_KEY_LOWER_CHUNK_IDENTIFIER: LazyLock = + LazyLock::new(|| ChunkIdentifier::new(0)); + +/// A [`ChunkIdentifier`] constructed with [`js_sys::Number::MAX_SAFE_INTEGER`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`ChunkIdentifier`]s when used in conjunction with +/// [`INDEXED_KEY_LOWER_CHUNK_IDENTIFIER`]. +pub static INDEXED_KEY_UPPER_CHUNK_IDENTIFIER: LazyLock = + LazyLock::new(|| ChunkIdentifier::new(js_sys::Number::MAX_SAFE_INTEGER as u64)); + +/// An [`OwnedEventId`] constructed with [`INDEXED_KEY_LOWER_CHARACTER`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`EventId`]s when used in conjunction with +/// [`INDEXED_KEY_UPPER_EVENT_ID`]. +pub static INDEXED_KEY_LOWER_EVENT_ID: LazyLock = LazyLock::new(|| { + OwnedEventId::try_from(format!("${INDEXED_KEY_LOWER_CHARACTER}")).expect("valid event id") +}); + +/// An [`OwnedEventId`] constructed with [`INDEXED_KEY_UPPER_CHARACTER`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`EventId`]s when used in conjunction with +/// [`INDEXED_KEY_LOWER_EVENT_ID`]. +pub static INDEXED_KEY_UPPER_EVENT_ID: LazyLock = LazyLock::new(|| { + OwnedEventId::try_from(format!("${INDEXED_KEY_UPPER_CHARACTER}")).expect("valid event id") +}); + +/// The lowest possible index that can be used to reference an [`Event`] inside +/// a [`Chunk`] - i.e., `0`. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`Position`]s when used in conjunction with +/// [`INDEXED_KEY_UPPER_EVENT_INDEX`]. +pub const INDEXED_KEY_LOWER_EVENT_INDEX: usize = 0; + +/// The highest possible index that can be used to reference an [`Event`] inside +/// a [`Chunk`] - i.e., [`js_sys::Number::MAX_SAFE_INTEGER`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`Position`]s when used in conjunction with +/// [`INDEXED_KEY_LOWER_EVENT_INDEX`]. +pub const INDEXED_KEY_UPPER_EVENT_INDEX: usize = js_sys::Number::MAX_SAFE_INTEGER as usize; + +/// The lowest possible [`Position`] that can be used to reference an [`Event`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`Position`]s when used in conjunction with +/// [`INDEXED_KEY_UPPER_EVENT_INDEX`]. +pub static INDEXED_KEY_LOWER_EVENT_POSITION: LazyLock = LazyLock::new(|| Position { + chunk_identifier: INDEXED_KEY_LOWER_CHUNK_IDENTIFIER.index(), + index: INDEXED_KEY_LOWER_EVENT_INDEX, +}); + +/// The highest possible [`Position`] that can be used to reference an +/// [`Event`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`Position`]s when used in conjunction with +/// [`INDEXED_KEY_LOWER_EVENT_INDEX`]. +pub static INDEXED_KEY_UPPER_EVENT_POSITION: LazyLock = LazyLock::new(|| Position { + chunk_identifier: INDEXED_KEY_UPPER_CHUNK_IDENTIFIER.index(), + index: INDEXED_KEY_UPPER_EVENT_INDEX, +}); diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs similarity index 70% rename from crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs rename to crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs index 5f955e6f3c5..c616d4d4dc4 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/indexed_types.rs @@ -27,217 +27,31 @@ //! These types mimic the structure of the object stores and indices created in //! [`crate::event_cache_store::migrations`]. -use std::sync::LazyLock; - use matrix_sdk_base::linked_chunk::{ChunkIdentifier, LinkedChunkId}; use matrix_sdk_crypto::CryptoStoreError; -use ruma::{events::relation::RelationType, EventId, OwnedEventId, RoomId}; +use ruma::{events::relation::RelationType, EventId, RoomId}; use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{ event_cache_store::{ migrations::current::keys, - serializer::traits::{ - Indexed, IndexedKey, IndexedKeyBounds, IndexedKeyComponentBounds, - IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds, + serializer::constants::{ + INDEXED_KEY_LOWER_CHUNK_IDENTIFIER, INDEXED_KEY_LOWER_EVENT_ID, + INDEXED_KEY_LOWER_EVENT_INDEX, INDEXED_KEY_LOWER_EVENT_POSITION, + INDEXED_KEY_UPPER_CHUNK_IDENTIFIER, INDEXED_KEY_UPPER_EVENT_ID, + INDEXED_KEY_UPPER_EVENT_INDEX, INDEXED_KEY_UPPER_EVENT_POSITION, }, types::{Chunk, Event, Gap, Lease, Position}, }, - serializer::{IndexeddbSerializer, MaybeEncrypted}, + serializer::{ + Indexed, IndexedKey, IndexedKeyComponentBounds, IndexedPrefixKeyBounds, + IndexedPrefixKeyComponentBounds, MaybeEncrypted, SafeEncodeSerializer, + INDEXED_KEY_LOWER_CHARACTER, INDEXED_KEY_LOWER_STRING, INDEXED_KEY_UPPER_CHARACTER, + INDEXED_KEY_UPPER_STRING, + }, }; -/// The first unicode character, and hence the lower bound for IndexedDB keys -/// (or key components) which are represented as strings. -/// -/// This value is useful for constructing a key range over all strings when used -/// in conjunction with [`INDEXED_KEY_UPPER_CHARACTER`]. -const INDEXED_KEY_LOWER_CHARACTER: char = '\u{0000}'; - -/// The last unicode character in the [Basic Multilingual Plane][1]. This seems -/// like a reasonable place to set the upper bound for IndexedDB keys (or key -/// components) which are represented as strings, though one could -/// theoretically set it to `\u{10FFFF}`. -/// -/// This value is useful for constructing a key range over all strings when used -/// in conjunction with [`INDEXED_KEY_LOWER_CHARACTER`]. -/// -/// [1]: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane -const INDEXED_KEY_UPPER_CHARACTER: char = '\u{FFFF}'; - -/// Identical to [`INDEXED_KEY_LOWER_CHARACTER`] but represented as a [`String`] -static INDEXED_KEY_LOWER_STRING: LazyLock = - LazyLock::new(|| String::from(INDEXED_KEY_LOWER_CHARACTER)); - -/// Identical to [`INDEXED_KEY_UPPER_CHARACTER`] but represented as a [`String`] -static INDEXED_KEY_UPPER_STRING: LazyLock = - LazyLock::new(|| String::from(INDEXED_KEY_UPPER_CHARACTER)); - -/// A [`ChunkIdentifier`] constructed with `0`. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`ChunkIdentifier`]s when used in conjunction with -/// [`INDEXED_KEY_UPPER_CHUNK_IDENTIFIER`]. -static INDEXED_KEY_LOWER_CHUNK_IDENTIFIER: LazyLock = - LazyLock::new(|| ChunkIdentifier::new(0)); - -/// A [`ChunkIdentifier`] constructed with [`js_sys::Number::MAX_SAFE_INTEGER`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`ChunkIdentifier`]s when used in conjunction with -/// [`INDEXED_KEY_LOWER_CHUNK_IDENTIFIER`]. -static INDEXED_KEY_UPPER_CHUNK_IDENTIFIER: LazyLock = - LazyLock::new(|| ChunkIdentifier::new(js_sys::Number::MAX_SAFE_INTEGER as u64)); - -/// An [`OwnedEventId`] constructed with [`INDEXED_KEY_LOWER_CHARACTER`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`EventId`]s when used in conjunction with -/// [`INDEXED_KEY_UPPER_EVENT_ID`]. -static INDEXED_KEY_LOWER_EVENT_ID: LazyLock = LazyLock::new(|| { - OwnedEventId::try_from(format!("${INDEXED_KEY_LOWER_CHARACTER}")).expect("valid event id") -}); - -/// An [`OwnedEventId`] constructed with [`INDEXED_KEY_UPPER_CHARACTER`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`EventId`]s when used in conjunction with -/// [`INDEXED_KEY_LOWER_EVENT_ID`]. -static INDEXED_KEY_UPPER_EVENT_ID: LazyLock = LazyLock::new(|| { - OwnedEventId::try_from(format!("${INDEXED_KEY_UPPER_CHARACTER}")).expect("valid event id") -}); - -/// The lowest possible index that can be used to reference an [`Event`] inside -/// a [`Chunk`] - i.e., `0`. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`Position`]s when used in conjunction with -/// [`INDEXED_KEY_UPPER_EVENT_INDEX`]. -const INDEXED_KEY_LOWER_EVENT_INDEX: usize = 0; - -/// The highest possible index that can be used to reference an [`Event`] inside -/// a [`Chunk`] - i.e., [`js_sys::Number::MAX_SAFE_INTEGER`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`Position`]s when used in conjunction with -/// [`INDEXED_KEY_LOWER_EVENT_INDEX`]. -const INDEXED_KEY_UPPER_EVENT_INDEX: usize = js_sys::Number::MAX_SAFE_INTEGER as usize; - -/// The lowest possible [`Position`] that can be used to reference an [`Event`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`Position`]s when used in conjunction with -/// [`INDEXED_KEY_UPPER_EVENT_INDEX`]. -static INDEXED_KEY_LOWER_EVENT_POSITION: LazyLock = LazyLock::new(|| Position { - chunk_identifier: INDEXED_KEY_LOWER_CHUNK_IDENTIFIER.index(), - index: INDEXED_KEY_LOWER_EVENT_INDEX, -}); - -/// The highest possible [`Position`] that can be used to reference an -/// [`Event`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`Position`]s when used in conjunction with -/// [`INDEXED_KEY_LOWER_EVENT_INDEX`]. -static INDEXED_KEY_UPPER_EVENT_POSITION: LazyLock = LazyLock::new(|| Position { - chunk_identifier: INDEXED_KEY_UPPER_CHUNK_IDENTIFIER.index(), - index: INDEXED_KEY_UPPER_EVENT_INDEX, -}); - -/// Representation of a range of keys of type `K`. This is loosely -/// correlated with [IDBKeyRange][1], with a few differences. -/// -/// Namely, this enum only provides a single way to express a bounded range -/// which is always inclusive on both bounds. While all ranges can still be -/// represented, [`IDBKeyRange`][1] provides more flexibility in this regard. -/// -/// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange -#[derive(Debug, Copy, Clone)] -pub enum IndexedKeyRange { - /// Represents a single key of type `K`. - /// - /// Identical to [`IDBKeyRange.only`][1]. - /// - /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/only - Only(K), - /// Represents an inclusive range of keys of type `K` - /// where the first item is the lower bound and the - /// second item is the upper bound. - /// - /// Similar to [`IDBKeyRange.bound`][1]. - /// - /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/bound - Bound(K, K), -} - -impl<'a, C: 'a> IndexedKeyRange { - /// Encodes a range of key components of type `K::KeyComponents` - /// into a range of keys of type `K`. - pub fn encoded(self, serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedKey = C>, - { - match self { - Self::Only(components) => IndexedKeyRange::Only(K::encode(components, serializer)), - Self::Bound(lower, upper) => { - IndexedKeyRange::Bound(K::encode(lower, serializer), K::encode(upper, serializer)) - } - } - } -} - -impl IndexedKeyRange { - pub fn map(self, f: F) -> IndexedKeyRange - where - F: Fn(K) -> T, - { - match self { - IndexedKeyRange::Only(key) => IndexedKeyRange::Only(f(key)), - IndexedKeyRange::Bound(lower, upper) => IndexedKeyRange::Bound(f(lower), f(upper)), - } - } - - pub fn all(serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedKeyBounds, - { - IndexedKeyRange::Bound(K::lower_key(serializer), K::upper_key(serializer)) - } - - pub fn all_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedPrefixKeyBounds, - P: Clone, - { - IndexedKeyRange::Bound( - K::lower_key_with_prefix(prefix.clone(), serializer), - K::upper_key_with_prefix(prefix, serializer), - ) - } -} - -impl From<(K, K)> for IndexedKeyRange { - fn from(value: (K, K)) -> Self { - Self::Bound(value.0, value.1) - } -} - -impl From for IndexedKeyRange { - fn from(value: K) -> Self { - Self::Only(value) - } -} - -/// 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; @@ -275,10 +89,6 @@ pub type IndexedEventContent = MaybeEncrypted; /// A (possibly) encrypted representation of a [`Gap`] pub type IndexedGapContent = MaybeEncrypted; -/// A representation of time in seconds since the [Unix -/// Epoch](std::time::UNIX_EPOCH) which is suitable for use in an IndexedDB key -pub type IndexedSecondsSinceUnixEpoch = u64; - /// Represents the [`LEASES`][1] object store. /// /// [1]: crate::event_cache_store::migrations::v1::create_lease_object_store @@ -299,7 +109,7 @@ impl Indexed for Lease { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(IndexedLease { id: >::encode(&self.key, serializer), @@ -309,7 +119,7 @@ impl Indexed for Lease { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content) } @@ -326,7 +136,7 @@ pub type IndexedLeaseIdKey = String; impl IndexedKey for IndexedLeaseIdKey { type KeyComponents<'a> = &'a str; - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { serializer.encode_key_as_string(keys::LEASES, components) } } @@ -364,7 +174,7 @@ impl Indexed for Chunk { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(IndexedChunk { id: >::encode( @@ -381,7 +191,7 @@ impl Indexed for Chunk { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content) } @@ -402,7 +212,7 @@ impl IndexedKey for IndexedChunkIdKey { fn encode( (linked_chunk_id, chunk_id): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let linked_chunk_id = serializer.hash_key(keys::LINKED_CHUNK_IDS, linked_chunk_id.storage_key()); @@ -462,7 +272,7 @@ impl IndexedKey for IndexedNextChunkIdKey { fn encode( (linked_chunk_id, next_chunk_id): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { next_chunk_id .map(|id| { @@ -529,7 +339,7 @@ impl Indexed for Event { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { let event_id = self.event_id().ok_or(Self::Error::NoEventId)?; let id = IndexedEventIdKey::encode((self.linked_chunk_id(), &event_id), serializer); @@ -554,7 +364,7 @@ impl Indexed for Event { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content).map_err(Into::into) } @@ -575,7 +385,7 @@ impl IndexedKey for IndexedEventIdKey { fn encode( (linked_chunk_id, event_id): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let linked_chunk_id = serializer.hash_key(keys::LINKED_CHUNK_IDS, linked_chunk_id.storage_key()); @@ -587,14 +397,14 @@ impl IndexedKey for IndexedEventIdKey { impl IndexedPrefixKeyBounds> for IndexedEventIdKey { fn lower_key_with_prefix( linked_chunk_id: LinkedChunkId<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { Self::encode((linked_chunk_id, &*INDEXED_KEY_LOWER_EVENT_ID), serializer) } fn upper_key_with_prefix( linked_chunk_id: LinkedChunkId<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { Self::encode((linked_chunk_id, &*INDEXED_KEY_UPPER_EVENT_ID), serializer) } @@ -617,7 +427,7 @@ impl IndexedKey for IndexedEventRoomKey { fn encode( (room_id, event_id): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id.as_str()); let event_id = serializer.encode_key_as_string(keys::EVENTS, event_id); @@ -626,11 +436,11 @@ impl IndexedKey for IndexedEventRoomKey { } impl IndexedPrefixKeyBounds for IndexedEventRoomKey { - fn lower_key_with_prefix(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + fn lower_key_with_prefix(room_id: &RoomId, serializer: &SafeEncodeSerializer) -> Self { Self::encode((room_id, &*INDEXED_KEY_LOWER_EVENT_ID), serializer) } - fn upper_key_with_prefix(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + fn upper_key_with_prefix(room_id: &RoomId, serializer: &SafeEncodeSerializer) -> Self { Self::encode((room_id, &*INDEXED_KEY_UPPER_EVENT_ID), serializer) } } @@ -653,7 +463,7 @@ impl IndexedKey for IndexedEventPositionKey { fn encode( (linked_chunk_id, position): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let linked_chunk_id = serializer.hash_key(keys::LINKED_CHUNK_IDS, linked_chunk_id.storage_key()); @@ -715,7 +525,7 @@ impl IndexedKey for IndexedEventRelationKey { fn encode( (room_id, related_event_id, relation_type): Self::KeyComponents<'_>, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); let related_event_id = @@ -727,14 +537,14 @@ impl IndexedKey for IndexedEventRelationKey { } impl IndexedPrefixKeyBounds for IndexedEventRelationKey { - fn lower_key_with_prefix(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + fn lower_key_with_prefix(room_id: &RoomId, serializer: &SafeEncodeSerializer) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); let related_event_id = String::from(INDEXED_KEY_LOWER_CHARACTER); let relation_type = String::from(INDEXED_KEY_LOWER_CHARACTER); Self(room_id, related_event_id, relation_type) } - fn upper_key_with_prefix(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + fn upper_key_with_prefix(room_id: &RoomId, serializer: &SafeEncodeSerializer) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); let related_event_id = String::from(INDEXED_KEY_UPPER_CHARACTER); let relation_type = String::from(INDEXED_KEY_UPPER_CHARACTER); @@ -745,7 +555,7 @@ impl IndexedPrefixKeyBounds for IndexedEventRelationKey { impl IndexedPrefixKeyBounds for IndexedEventRelationKey { fn lower_key_with_prefix( (room_id, related_event_id): (&RoomId, &EventId), - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); let related_event_id = @@ -756,7 +566,7 @@ impl IndexedPrefixKeyBounds for IndexedEventRelation fn upper_key_with_prefix( (room_id, related_event_id): (&RoomId, &EventId), - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Self { let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); let related_event_id = @@ -785,7 +595,7 @@ impl Indexed for Gap { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(IndexedGap { id: >::encode( @@ -798,7 +608,7 @@ impl Indexed for Gap { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content) } @@ -815,7 +625,7 @@ pub type IndexedGapIdKey = IndexedChunkIdKey; impl IndexedKey for IndexedGapIdKey { type KeyComponents<'a> = >::KeyComponents<'a>; - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { >::encode(components, serializer) } } 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 bc091a00d1c..967ed2e9e1e 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 @@ -12,157 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License -use gloo_utils::format::JsValueSerdeExt; -use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; -use wasm_bindgen::JsValue; -use web_sys::IdbKeyRange; - -use crate::{ - event_cache_store::serializer::{ - traits::{Indexed, IndexedKey}, - types::IndexedKeyRange, - }, - serializer::IndexeddbSerializer, -}; - -pub mod traits; -pub mod types; - -#[derive(Debug, Error)] -pub enum IndexeddbEventCacheStoreSerializerError { - #[error("indexing: {0}")] - Indexing(IndexingError), - #[error("serialization: {0}")] - Serialization(#[from] serde_json::Error), -} - -impl From for IndexeddbEventCacheStoreSerializerError { - fn from(e: serde_wasm_bindgen::Error) -> Self { - Self::Serialization(serde::de::Error::custom(e.to_string())) - } -} - -/// A (de)serializer for an IndexedDB implementation of [`EventCacheStore`][1]. -/// -/// This is primarily a wrapper around [`IndexeddbSerializer`] with a -/// convenience functions for (de)serializing types specific to the -/// [`EventCacheStore`][1]. -/// -/// [1]: matrix_sdk_base::event_cache::store::EventCacheStore -#[derive(Debug, Clone)] -pub struct IndexeddbEventCacheStoreSerializer { - inner: IndexeddbSerializer, -} - -impl IndexeddbEventCacheStoreSerializer { - pub fn new(inner: IndexeddbSerializer) -> Self { - Self { inner } - } - - /// Returns a reference to the inner [`IndexeddbSerializer`]. - pub fn inner(&self) -> &IndexeddbSerializer { - &self.inner - } - - /// Encodes an key for a [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key(&self, components: K::KeyComponents<'_>) -> K - where - T: Indexed, - K: IndexedKey, - { - K::encode(components, &self.inner) - } - - /// Encodes a key for a [`Indexed`] type as a [`JsValue`]. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_as_value( - &self, - components: K::KeyComponents<'_>, - ) -> Result - where - T: Indexed, - K: IndexedKey + Serialize, - { - serde_wasm_bindgen::to_value(&self.encode_key::(components)) - } - - /// Encodes a key component range for an [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_range( - &self, - range: impl Into>, - ) -> Result - where - T: Indexed, - K: Serialize, - { - use serde_wasm_bindgen::to_value; - Ok(match range.into() { - IndexedKeyRange::Only(key) => IdbKeyRange::only(&to_value(&key)?)?, - IndexedKeyRange::Bound(lower, upper) => { - IdbKeyRange::bound(&to_value(&lower)?, &to_value(&upper)?)? - } - }) - } - - /// Encodes a key component range for an [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_component_range<'a, T, K>( - &self, - range: impl Into>>, - ) -> Result - where - T: Indexed, - K: IndexedKey + Serialize, - { - let range = match range.into() { - IndexedKeyRange::Only(components) => { - IndexedKeyRange::Only(K::encode(components, &self.inner)) - } - IndexedKeyRange::Bound(lower, upper) => { - let lower = K::encode(lower, &self.inner); - let upper = K::encode(upper, &self.inner); - IndexedKeyRange::Bound(lower, upper) - } - }; - self.encode_key_range::(range) - } - - /// Serializes an [`Indexed`] type into a [`JsValue`] - pub fn serialize( - &self, - t: &T, - ) -> Result> - where - T: Indexed, - T::IndexedType: Serialize, - { - let indexed = - t.to_indexed(&self.inner).map_err(IndexeddbEventCacheStoreSerializerError::Indexing)?; - serde_wasm_bindgen::to_value(&indexed).map_err(Into::into) - } - - /// Deserializes an [`Indexed`] type from a [`JsValue`] - pub fn deserialize( - &self, - value: JsValue, - ) -> Result> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - { - let indexed: T::IndexedType = value.into_serde()?; - T::from_indexed(indexed, &self.inner) - .map_err(IndexeddbEventCacheStoreSerializerError::Indexing) - } -} +pub mod constants; +pub mod indexed_types; 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 81d4fde0d64..73e2048fb17 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/transaction.rs @@ -12,136 +12,55 @@ // See the License for the specific language governing permissions and // limitations under the License -use indexed_db_futures::{prelude::IdbTransaction, IdbQuerySource}; +use std::ops::Deref; + +use indexed_db_futures::prelude::IdbTransaction; use matrix_sdk_base::{ - event_cache::{store::EventCacheStoreError, Event as RawEvent, Gap as RawGap}, + event_cache::{Event as RawEvent, Gap as RawGap}, linked_chunk::{ChunkContent, ChunkIdentifier, LinkedChunkId, RawChunk}, }; use ruma::{events::relation::RelationType, EventId, RoomId}; -use serde::{ - de::{DeserializeOwned, Error}, - Serialize, -}; -use thiserror::Error; -use web_sys::IdbCursorDirection; +use serde::{de::DeserializeOwned, Serialize}; -use crate::event_cache_store::{ +use crate::{ error::AsyncErrorDeps, - serializer::{ - traits::{Indexed, IndexedKey, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds}, - types::{ + event_cache_store::{ + serializer::indexed_types::{ IndexedChunkIdKey, IndexedEventIdKey, IndexedEventPositionKey, IndexedEventRelationKey, - IndexedEventRoomKey, IndexedGapIdKey, IndexedKeyRange, IndexedLeaseIdKey, - IndexedNextChunkIdKey, IndexedRoomId, + IndexedEventRoomKey, IndexedGapIdKey, IndexedLeaseIdKey, IndexedNextChunkIdKey, }, - IndexeddbEventCacheStoreSerializer, + types::{Chunk, ChunkType, Event, Gap, Lease, Position}, }, - types::{Chunk, ChunkType, Event, Gap, Lease, Position}, + serializer::{ + Indexed, IndexedKeyRange, IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds, + IndexedTypeSerializer, + }, + transaction::{Transaction, TransactionError}, }; -#[derive(Debug, Error)] -pub enum IndexeddbEventCacheStoreTransactionError { - #[error("DomException {name} ({code}): {message}")] - DomException { name: String, message: String, code: u16 }, - #[error("serialization: {0}")] - Serialization(Box), - #[error("item is not unique")] - ItemIsNotUnique, - #[error("item not found")] - ItemNotFound, -} - -impl From for IndexeddbEventCacheStoreTransactionError { - fn from(value: web_sys::DomException) -> Self { - Self::DomException { name: value.name(), message: value.message(), code: value.code() } - } -} - -impl From for IndexeddbEventCacheStoreTransactionError { - fn from(e: serde_wasm_bindgen::Error) -> Self { - Self::Serialization(Box::new(serde_json::Error::custom(e.to_string()))) - } -} - -impl From for EventCacheStoreError { - fn from(value: IndexeddbEventCacheStoreTransactionError) -> Self { - use IndexeddbEventCacheStoreTransactionError::*; - - match value { - DomException { .. } => Self::InvalidData { details: value.to_string() }, - Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())), - ItemIsNotUnique | ItemNotFound => Self::InvalidData { details: value.to_string() }, - } - } -} - /// Represents an IndexedDB transaction, but provides a convenient interface for /// performing operations relevant to the IndexedDB implementation of /// [`EventCacheStore`](matrix_sdk_base::event_cache::store::EventCacheStore). pub struct IndexeddbEventCacheStoreTransaction<'a> { - transaction: IdbTransaction<'a>, - serializer: &'a IndexeddbEventCacheStoreSerializer, + transaction: Transaction<'a>, } -impl<'a> IndexeddbEventCacheStoreTransaction<'a> { - pub fn new( - transaction: IdbTransaction<'a>, - serializer: &'a IndexeddbEventCacheStoreSerializer, - ) -> Self { - Self { transaction, serializer } - } - - /// Returns the underlying IndexedDB transaction. - pub fn into_inner(self) -> IdbTransaction<'a> { - self.transaction - } +impl<'a> Deref for IndexeddbEventCacheStoreTransaction<'a> { + type Target = Transaction<'a>; - /// Commit all operations tracked in this transaction to IndexedDB. - pub async fn commit(self) -> Result<(), IndexeddbEventCacheStoreTransactionError> { - self.transaction.await.into_result().map_err(Into::into) + fn deref(&self) -> &Self::Target { + &self.transaction } +} - /// Query IndexedDB for items that match the given key range - pub async fn get_items_by_key( - &self, - range: impl Into>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - let array = if let Some(index) = K::INDEX { - object_store.index(index)?.get_all_with_key(&range)?.await? - } else { - object_store.get_all_with_key(&range)?.await? - }; - let mut items = Vec::with_capacity(array.length() as usize); - for value in array { - let item = self.serializer.deserialize(value).map_err(|e| { - IndexeddbEventCacheStoreTransactionError::Serialization(Box::new(e)) - })?; - items.push(item); - } - Ok(items) +impl<'a> IndexeddbEventCacheStoreTransaction<'a> { + pub fn new(transaction: IdbTransaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self { + Self { transaction: Transaction::new(transaction, serializer) } } - /// Query IndexedDB for items that match the given key component range - pub async fn get_items_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.get_items_by_key::(range).await + /// Commit all operations tracked in this transaction to IndexedDB. + pub async fn commit(self) -> Result<(), TransactionError> { + self.transaction.commit().await } /// Query IndexedDB for all items matching the given linked chunk id by key @@ -149,7 +68,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn get_items_by_linked_chunk_id<'b, T, K>( &self, linked_chunk_id: LinkedChunkId<'b>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> + ) -> Result, TransactionError> where T: Indexed, T::IndexedType: DeserializeOwned, @@ -158,7 +77,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { { self.get_items_by_key::(IndexedKeyRange::all_with_prefix( linked_chunk_id, - self.serializer.inner(), + self.serializer().inner(), )) .await } @@ -167,7 +86,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn get_items_in_room<'b, T, K>( &self, room_id: &'b RoomId, - ) -> Result, IndexeddbEventCacheStoreTransactionError> + ) -> Result, TransactionError> where T: Indexed, T::IndexedType: DeserializeOwned, @@ -176,92 +95,17 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { { self.get_items_by_key::(IndexedKeyRange::all_with_prefix( room_id, - self.serializer.inner(), + self.serializer().inner(), )) .await } - /// Query IndexedDB for items that match the given key. If - /// more than one item is found, an error is returned. - pub async fn get_item_by_key( - &self, - key: K, - ) -> Result, IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let mut items = self.get_items_by_key::(key).await?; - if items.len() > 1 { - return Err(IndexeddbEventCacheStoreTransactionError::ItemIsNotUnique); - } - Ok(items.pop()) - } - - /// Query IndexedDB for items that match the given key components. If more - /// than one item is found, an error is returned. - pub async fn get_item_by_key_components<'b, T, K>( - &self, - components: K::KeyComponents<'b>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let mut items = self.get_items_by_key_components::(components).await?; - if items.len() > 1 { - return Err(IndexeddbEventCacheStoreTransactionError::ItemIsNotUnique); - } - Ok(items.pop()) - } - - /// Query IndexedDB for the number of items that match the given key range. - pub async fn get_items_count_by_key( - &self, - range: impl Into>, - ) -> Result - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - let count = if let Some(index) = K::INDEX { - object_store.index(index)?.count_with_key(&range)?.await? - } else { - object_store.count_with_key(&range)?.await? - }; - Ok(count as usize) - } - - /// Query IndexedDB for the number of items that match the given key - /// components range. - pub async fn get_items_count_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.get_items_count_by_key::(range).await - } - /// Query IndexedDB for the number of items matching the given linked chunk /// id. pub async fn get_items_count_by_linked_chunk_id<'b, T, K>( &self, linked_chunk_id: LinkedChunkId<'b>, - ) -> Result + ) -> Result where T: Indexed, T::IndexedType: DeserializeOwned, @@ -270,213 +114,37 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { { self.get_items_count_by_key::(IndexedKeyRange::all_with_prefix( linked_chunk_id, - self.serializer.inner(), + self.serializer().inner(), )) .await } - /// Query IndexedDB for the number of items of type `T` by `K` in the given - /// room. - pub async fn get_items_count_in_room<'b, T, K>( - &self, - room_id: &'b RoomId, - ) -> Result - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedPrefixKeyBounds + Serialize, - { - self.get_items_count_by_key::(IndexedKeyRange::all_with_prefix( - room_id, - self.serializer.inner(), - )) - .await - } - - /// Query IndexedDB for the item with the maximum key in the given range. - pub async fn get_max_item_by_key( - &self, - range: impl Into>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let direction = IdbCursorDirection::Prev; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - if let Some(index) = K::INDEX { - object_store - .index(index)? - .open_cursor_with_range_and_direction(&range, direction)? - .await? - .map(|cursor| self.serializer.deserialize(cursor.value())) - .transpose() - .map_err(|e| IndexeddbEventCacheStoreTransactionError::Serialization(Box::new(e))) - } else { - object_store - .open_cursor_with_range_and_direction(&range, direction)? - .await? - .map(|cursor| self.serializer.deserialize(cursor.value())) - .transpose() - .map_err(|e| IndexeddbEventCacheStoreTransactionError::Serialization(Box::new(e))) - } - } - - /// Adds an item to the corresponding IndexedDB object - /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already - /// exists, it will be rejected. - pub async fn add_item( - &self, - item: &T, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + Serialize, - T::IndexedType: Serialize, - T::Error: AsyncErrorDeps, - { - self.transaction - .object_store(T::OBJECT_STORE)? - .add_val_owned(self.serializer.serialize(item).map_err(|e| { - IndexeddbEventCacheStoreTransactionError::Serialization(Box::new(e)) - })?)? - .await - .map_err(Into::into) - } - - /// Puts an item in the corresponding IndexedDB object - /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already - /// exists, it will be overwritten. - pub async fn put_item( - &self, - item: &T, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + Serialize, - T::IndexedType: Serialize, - T::Error: AsyncErrorDeps, - { - self.transaction - .object_store(T::OBJECT_STORE)? - .put_val_owned(self.serializer.serialize(item).map_err(|e| { - IndexeddbEventCacheStoreTransactionError::Serialization(Box::new(e)) - })?)? - .await - .map_err(Into::into) - } - - /// Delete items in given key range from IndexedDB - pub async fn delete_items_by_key( - &self, - range: impl Into>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - if let Some(index) = K::INDEX { - let index = object_store.index(index)?; - if let Some(cursor) = index.open_cursor_with_range(&range)?.await? { - while cursor.key().is_some() { - cursor.delete()?.await?; - cursor.continue_cursor()?.await?; - } - } - } else { - object_store.delete_owned(&range)?.await?; - } - Ok(()) - } - - /// Delete items in the given key component range from - /// IndexedDB - pub async fn delete_items_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + 'b, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.delete_items_by_key::(range).await - } - /// Delete all items of type `T` by key `K` associated with the given linked /// chunk id from IndexedDB pub async fn delete_items_by_linked_chunk_id<'b, T, K>( &self, linked_chunk_id: LinkedChunkId<'b>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> + ) -> Result<(), TransactionError> where T: Indexed, K: IndexedPrefixKeyBounds> + Serialize, { self.delete_items_by_key::(IndexedKeyRange::all_with_prefix( linked_chunk_id, - self.serializer.inner(), - )) - .await - } - - /// Delete all items of type `T` by key `K` in the given room from IndexedDB - pub async fn delete_items_in_room<'b, T, K>( - &self, - room_id: &'b RoomId, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - K: IndexedPrefixKeyBounds + Serialize, - { - self.delete_items_by_key::(IndexedKeyRange::all_with_prefix( - room_id, - self.serializer.inner(), + self.serializer().inner(), )) .await } - /// Delete item that matches the given key components from - /// IndexedDB - pub async fn delete_item_by_key<'b, T, K>( - &self, - key: K::KeyComponents<'b>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed + 'b, - K: IndexedKey + Serialize + 'b, - { - self.delete_items_by_key_components::(key).await - } - - /// Clear all items of type `T` from the associated object store - /// `T::OBJECT_STORE` from IndexedDB - pub async fn clear(&self) -> Result<(), IndexeddbEventCacheStoreTransactionError> - where - T: Indexed, - { - self.transaction.object_store(T::OBJECT_STORE)?.clear()?.await.map_err(Into::into) - } - /// Query IndexedDB for the lease that matches the given key `id`. If more /// than one lease is found, an error is returned. - pub async fn get_lease_by_id( - &self, - id: &str, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + pub async fn get_lease_by_id(&self, id: &str) -> Result, TransactionError> { self.get_item_by_key_components::(id).await } /// Puts a lease into IndexedDB. If an event with the same key already /// exists, it will be overwritten. - pub async fn put_lease( - &self, - lease: &Lease, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + pub async fn put_lease(&self, lease: &Lease) -> Result<(), TransactionError> { self.put_item(lease).await } @@ -487,7 +155,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + ) -> Result, TransactionError> { self.get_item_by_key_components::((linked_chunk_id, chunk_id)) .await } @@ -499,7 +167,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, next_chunk_id: Option, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + ) -> Result, TransactionError> { self.get_item_by_key_components::(( linked_chunk_id, next_chunk_id, @@ -511,7 +179,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn get_chunks_by_linked_chunk_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + ) -> Result, TransactionError> { self.get_items_by_linked_chunk_id::(linked_chunk_id).await } @@ -520,7 +188,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn get_chunks_count_by_linked_chunk_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result { + ) -> Result { self.get_items_count_by_linked_chunk_id::(linked_chunk_id).await } @@ -529,9 +197,11 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn get_max_chunk_by_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - let range = - IndexedKeyRange::all_with_prefix::(linked_chunk_id, self.serializer.inner()); + ) -> Result, TransactionError> { + let range = IndexedKeyRange::all_with_prefix::( + linked_chunk_id, + self.serializer().inner(), + ); self.get_max_item_by_key::(range).await } @@ -542,7 +212,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result>, IndexeddbEventCacheStoreTransactionError> { + ) -> Result>, TransactionError> { if let Some(chunk) = self.get_chunk_by_id(linked_chunk_id, chunk_id).await? { let content = match chunk.chunk_type { ChunkType::Event => { @@ -561,7 +231,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { let gap = self .get_gap_by_id(linked_chunk_id, ChunkIdentifier::new(chunk.identifier)) .await? - .ok_or(IndexeddbEventCacheStoreTransactionError::ItemNotFound)?; + .ok_or(TransactionError::ItemNotFound)?; ChunkContent::Gap(RawGap { prev_token: gap.prev_token }) } }; @@ -579,10 +249,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { /// chunks are properly linked to the chunk being added. If a chunk with /// the same identifier already exists, the given chunk will be /// rejected. - pub async fn add_chunk( - &self, - chunk: &Chunk, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + pub async fn add_chunk(&self, chunk: &Chunk) -> Result<(), TransactionError> { self.add_item(chunk).await?; if let Some(previous) = chunk.previous { let previous_identifier = ChunkIdentifier::new(previous); @@ -613,7 +280,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { if let Some(chunk) = self.get_chunk_by_id(linked_chunk_id, chunk_id).await? { if let Some(previous) = chunk.previous { let previous_identifier = ChunkIdentifier::new(previous); @@ -651,7 +318,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn delete_chunks_by_linked_chunk_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_items_by_linked_chunk_id::(linked_chunk_id).await } @@ -661,8 +328,8 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, event_id: &EventId, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - let key = self.serializer.encode_key((linked_chunk_id, event_id)); + ) -> Result, TransactionError> { + let key = self.serializer().encode_key((linked_chunk_id, event_id)); self.get_item_by_key::(key).await } @@ -672,55 +339,28 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, room_id: &RoomId, event_id: &EventId, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - let key = self.serializer.encode_key((room_id, event_id)); + ) -> Result, TransactionError> { + let key = self.serializer().encode_key((room_id, event_id)); self.get_item_by_key::(key).await } /// Query IndexedDB for events that are in the given /// room. - pub async fn get_room_events( - &self, - room_id: &RoomId, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + pub async fn get_room_events(&self, room_id: &RoomId) -> Result, TransactionError> { self.get_items_in_room::(room_id).await } - /// Query IndexedDB for events in the given position range matching the - /// given linked chunk id. - pub async fn get_events_by_position( - &self, - linked_chunk_id: LinkedChunkId<'_>, - range: impl Into>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - self.get_items_by_key_components::( - range.into().map(|position| (linked_chunk_id, position)), - ) - .await - } - - /// Query IndexedDB for number of events in the given position range - /// matching the given linked_chunk_id. - pub async fn get_events_count_by_position( - &self, - linked_chunk_id: LinkedChunkId<'_>, - range: impl Into>, - ) -> Result { - self.get_items_count_by_key_components::( - range.into().map(|position| (linked_chunk_id, position)), - ) - .await - } - /// Query IndexedDB for events in the given chunk matching the given linked /// chunk id. pub async fn get_events_by_chunk( &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - let range = - IndexedKeyRange::all_with_prefix((linked_chunk_id, chunk_id), self.serializer.inner()); + ) -> Result, TransactionError> { + let range = IndexedKeyRange::all_with_prefix( + (linked_chunk_id, chunk_id), + self.serializer().inner(), + ); self.get_items_by_key::(range).await } @@ -730,9 +370,11 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result { - let range = - IndexedKeyRange::all_with_prefix((linked_chunk_id, chunk_id), self.serializer.inner()); + ) -> Result { + let range = IndexedKeyRange::all_with_prefix( + (linked_chunk_id, chunk_id), + self.serializer().inner(), + ); self.get_items_count_by_key::(range).await } @@ -742,11 +384,11 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, room_id: &RoomId, range: impl Into>, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + ) -> Result, TransactionError> { let range = range .into() .map(|(event_id, relation_type)| (room_id, event_id, relation_type)) - .encoded(self.serializer.inner()); + .encoded(self.serializer().inner()); self.get_items_by_key::(range).await } @@ -756,18 +398,17 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, room_id: &RoomId, related_event_id: &EventId, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { - let range = - IndexedKeyRange::all_with_prefix((room_id, related_event_id), self.serializer.inner()); + ) -> Result, TransactionError> { + let range = IndexedKeyRange::all_with_prefix( + (room_id, related_event_id), + self.serializer().inner(), + ); self.get_items_by_key::(range).await } /// Puts an event in IndexedDB. If an event with the same key already /// exists, it will be overwritten. - pub async fn put_event( - &self, - event: &Event, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + pub async fn put_event(&self, event: &Event) -> Result<(), TransactionError> { if let Some(position) = event.position() { // For some reason, we can't simply replace an event with `put_item` // because we can get an error stating that the data violates a uniqueness @@ -789,7 +430,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, range: impl Into>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_items_by_key_components::( range.into().map(|position| (linked_chunk_id, position)), ) @@ -801,7 +442,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, position: Position, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_item_by_key::((linked_chunk_id, position)).await } @@ -810,9 +451,11 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { - let range = - IndexedKeyRange::all_with_prefix((linked_chunk_id, chunk_id), self.serializer.inner()); + ) -> Result<(), TransactionError> { + let range = IndexedKeyRange::all_with_prefix( + (linked_chunk_id, chunk_id), + self.serializer().inner(), + ); self.delete_items_by_key::(range).await } @@ -822,7 +465,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, position: Position, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { let lower = (linked_chunk_id, position); let upper = IndexedEventPositionKey::upper_key_components_with_prefix(( linked_chunk_id, @@ -836,7 +479,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn delete_events_by_linked_chunk_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_items_by_linked_chunk_id::(linked_chunk_id).await } @@ -846,7 +489,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result, IndexeddbEventCacheStoreTransactionError> { + ) -> Result, TransactionError> { self.get_item_by_key_components::((linked_chunk_id, chunk_id)).await } @@ -856,7 +499,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { &self, linked_chunk_id: LinkedChunkId<'_>, chunk_id: ChunkIdentifier, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_item_by_key::((linked_chunk_id, chunk_id)).await } @@ -864,7 +507,7 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> { pub async fn delete_gaps_by_linked_chunk_id( &self, linked_chunk_id: LinkedChunkId<'_>, - ) -> Result<(), IndexeddbEventCacheStoreTransactionError> { + ) -> Result<(), TransactionError> { self.delete_items_by_linked_chunk_id::(linked_chunk_id).await } } diff --git a/crates/matrix-sdk-indexeddb/src/lib.rs b/crates/matrix-sdk-indexeddb/src/lib.rs index 624f0e13ad8..c9e5c1de83b 100644 --- a/crates/matrix-sdk-indexeddb/src/lib.rs +++ b/crates/matrix-sdk-indexeddb/src/lib.rs @@ -6,17 +6,17 @@ use thiserror::Error; #[cfg(feature = "e2e-encryption")] mod crypto_store; +#[cfg(any(feature = "event-cache-store", feature = "media-store"))] +mod error; #[cfg(feature = "event-cache-store")] mod event_cache_store; #[cfg(feature = "media-store")] mod media_store; -mod safe_encode; -#[cfg(feature = "e2e-encryption")] -mod serialize_bool_for_indexeddb; -#[cfg(feature = "e2e-encryption")] mod serializer; #[cfg(feature = "state-store")] mod state_store; +#[cfg(any(feature = "event-cache-store", feature = "media-store"))] +mod transaction; #[cfg(feature = "e2e-encryption")] pub use crypto_store::{IndexeddbCryptoStore, IndexeddbCryptoStoreError}; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs index 8647d34cb03..70457c5c7c6 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/builder.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/builder.rs @@ -19,10 +19,9 @@ use matrix_sdk_store_encryption::StoreCipher; use crate::{ media_store::{ - error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, - serializer::IndexeddbMediaStoreSerializer, IndexeddbMediaStore, + error::IndexeddbMediaStoreError, migrations::open_and_upgrade_db, IndexeddbMediaStore, }, - serializer::IndexeddbSerializer, + serializer::{IndexedTypeSerializer, SafeEncodeSerializer}, }; /// A type for conveniently building an [`IndexeddbMediaStore`] @@ -66,9 +65,7 @@ impl IndexeddbMediaStoreBuilder { pub async fn build(self) -> Result { Ok(IndexeddbMediaStore { inner: Rc::new(open_and_upgrade_db(&self.database_name).await?), - serializer: IndexeddbMediaStoreSerializer::new(IndexeddbSerializer::new( - self.store_cipher, - )), + serializer: IndexedTypeSerializer::new(SafeEncodeSerializer::new(self.store_cipher)), media_service: MediaService::new(), memory_store: MemoryMediaStore::new(), }) diff --git a/crates/matrix-sdk-indexeddb/src/media_store/error.rs b/crates/matrix-sdk-indexeddb/src/media_store/error.rs index 4592c554e5a..ce52a25d622 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/error.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/error.rs @@ -12,21 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License -use matrix_sdk_base::{ - media::store::{MediaStore, MediaStoreError, MemoryMediaStore}, - SendOutsideWasm, SyncOutsideWasm, -}; +use matrix_sdk_base::media::store::{MediaStore, MediaStoreError, MemoryMediaStore}; +use serde::de::Error; use thiserror::Error; -use crate::media_store::transaction::IndexeddbMediaStoreTransactionError; - -/// A trait that combines the necessary traits needed for asynchronous runtimes, -/// but excludes them when running in a web environment - i.e., when -/// `#[cfg(target_family = "wasm")]`. -pub trait AsyncErrorDeps: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static {} - -impl AsyncErrorDeps for T where T: std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static -{} +use crate::transaction::TransactionError; #[derive(Debug, Error)] pub enum IndexeddbMediaStoreError { @@ -34,7 +24,7 @@ pub enum IndexeddbMediaStoreError { MemoryStore(::Error), #[error("transaction: {0}")] - Transaction(#[from] IndexeddbMediaStoreTransactionError), + Transaction(#[from] TransactionError), #[error("DomException {name} ({code}): {message}")] DomException { name: String, message: String, code: u16 }, @@ -57,3 +47,15 @@ impl From for IndexeddbMediaStoreError { Self::DomException { name: value.name(), message: value.message(), code: value.code() } } } + +impl From for MediaStoreError { + fn from(value: TransactionError) -> Self { + use TransactionError::*; + + match value { + DomException { .. } => Self::InvalidData { details: value.to_string() }, + Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())), + ItemIsNotUnique | ItemNotFound => Self::InvalidData { details: value.to_string() }, + } + } +} diff --git a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs index d8795531197..b12bcd5a84a 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License -#![cfg_attr(not(test), allow(unused))] +// Allow dead code here, as this module is still in the process +// of being developed, so some functions will be used later on. +// Once development is complete, we can remove this line and +// clean up any unused code. +#![allow(dead_code)] mod builder; mod error; @@ -36,12 +40,12 @@ use matrix_sdk_base::{ timer, }; use ruma::{time::SystemTime, MilliSecondsSinceUnixEpoch, MxcUri}; -use serializer::IndexeddbMediaStoreSerializer; use tracing::instrument; use web_sys::IdbTransactionMode; -use crate::media_store::{ - serializer::traits::Indexed, transaction::IndexeddbMediaStoreTransaction, types::Lease, +use crate::{ + media_store::{transaction::IndexeddbMediaStoreTransaction, types::Lease}, + serializer::{Indexed, IndexedTypeSerializer}, }; /// A type for providing an IndexedDB implementation of [`MediaStore`][1]. @@ -54,7 +58,7 @@ pub struct IndexeddbMediaStore { // A handle to the IndexedDB database inner: Rc, // A serializer with functionality tailored to `IndexeddbMediaStore` - serializer: IndexeddbMediaStoreSerializer, + serializer: IndexedTypeSerializer, // A service for conveniently delegating media-related queries to an `MediaStoreInner` // implementation media_service: MediaService, @@ -333,13 +337,12 @@ impl MediaStoreInner for IndexeddbMediaStore { #[cfg(all(test, target_family = "wasm"))] mod tests { use matrix_sdk_base::{ - media::store::{MediaStore, MediaStoreError}, - media_store_integration_tests, media_store_integration_tests_time, + media::store::MediaStoreError, media_store_integration_tests, + media_store_integration_tests_time, }; - use matrix_sdk_test::async_test; use uuid::Uuid; - use crate::media_store::{error::IndexeddbMediaStoreError, IndexeddbMediaStore}; + use crate::media_store::IndexeddbMediaStore; mod unencrypted { use super::*; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/constants.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/constants.rs new file mode 100644 index 00000000000..84845898905 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/media_store/serializer/constants.rs @@ -0,0 +1,32 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +use crate::media_store::serializer::indexed_types::IndexedMediaContentSize; + +/// An [`IndexedMediaContentSize`] set to it's minimal value - i.e., `0`. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`IndexedMediaContentSize`] values when used in conjunction with +/// [`INDEXED_KEY_UPPER_MEDIA_CONTENT_SIZE`]. +pub const INDEXED_KEY_LOWER_MEDIA_CONTENT_SIZE: IndexedMediaContentSize = 0; + +/// An [`IndexedMediaContentSize`] set to [`js_sys::Number::MAX_SAFE_INTEGER`]. +/// Note that this restricts the size of [`IndexedMedia::content`], which +/// ultimately restricts the size of [`Media::content`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain [`IndexedMediaContentSize`] values when used in conjunction with +/// [`INDEXED_KEY_LOWER_MEDIA_CONTENT_SIZE`]. +pub const INDEXED_KEY_UPPER_MEDIA_CONTENT_SIZE: IndexedMediaContentSize = + js_sys::Number::MAX_SAFE_INTEGER as usize; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/types.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs similarity index 71% rename from crates/matrix-sdk-indexeddb/src/media_store/serializer/types.rs rename to crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs index 1ab31ea899f..2e49e128cd4 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/serializer/types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/serializer/indexed_types.rs @@ -27,7 +27,7 @@ //! These types mimic the structure of the object stores and indices created in //! [`crate::media_store::migrations`]. -use std::{sync::LazyLock, time::Duration}; +use std::time::Duration; use matrix_sdk_base::media::{ store::{IgnoreMediaRetentionPolicy, MediaRetentionPolicy}, @@ -42,163 +42,20 @@ use crate::{ media_store::{ migrations::current::keys, serializer::{ - foreign::ignore_media_retention_policy, - traits::{ - Indexed, IndexedKey, IndexedKeyBounds, IndexedKeyComponentBounds, - IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds, + constants::{ + INDEXED_KEY_LOWER_MEDIA_CONTENT_SIZE, INDEXED_KEY_UPPER_MEDIA_CONTENT_SIZE, }, + foreign::ignore_media_retention_policy, }, types::{Lease, Media}, }, - serializer::{IndexeddbSerializer, MaybeEncrypted}, + serializer::{ + Indexed, IndexedKey, IndexedKeyComponentBounds, IndexedPrefixKeyComponentBounds, + MaybeEncrypted, SafeEncodeSerializer, INDEXED_KEY_LOWER_DURATION, INDEXED_KEY_LOWER_STRING, + INDEXED_KEY_UPPER_DURATION_SECONDS, INDEXED_KEY_UPPER_STRING, + }, }; -/// The first unicode character, and hence the lower bound for IndexedDB keys -/// (or key components) which are represented as strings. -/// -/// This value is useful for constructing a key range over all strings when used -/// in conjunction with [`INDEXED_KEY_UPPER_CHARACTER`]. -const INDEXED_KEY_LOWER_CHARACTER: char = '\u{0000}'; - -/// The last unicode character in the [Basic Multilingual Plane][1]. This seems -/// like a reasonable place to set the upper bound for IndexedDB keys (or key -/// components) which are represented as strings, though one could -/// theoretically set it to `\u{10FFFF}`. -/// -/// This value is useful for constructing a key range over all strings when used -/// in conjunction with [`INDEXED_KEY_LOWER_CHARACTER`]. -/// -/// [1]: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane -const INDEXED_KEY_UPPER_CHARACTER: char = '\u{FFFF}'; - -/// Identical to [`INDEXED_KEY_LOWER_CHARACTER`] but represented as a [`String`] -static INDEXED_KEY_LOWER_STRING: LazyLock = - LazyLock::new(|| String::from(INDEXED_KEY_LOWER_CHARACTER)); - -/// Identical to [`INDEXED_KEY_UPPER_CHARACTER`] but represented as a [`String`] -static INDEXED_KEY_UPPER_STRING: LazyLock = - LazyLock::new(|| String::from(INDEXED_KEY_UPPER_CHARACTER)); - -/// An [`IndexedMediaContentSize`] set to it's minimal value - i.e., `0`. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`IndexedMediaContentSize`] values when used in conjunction with -/// [`INDEXED_KEY_UPPER_MEDIA_CONTENT_SIZE`]. -const INDEXED_KEY_LOWER_MEDIA_CONTENT_SIZE: IndexedMediaContentSize = 0; - -/// An [`IndexedMediaContentSize`] set to [`js_sys::Number::MAX_SAFE_INTEGER`]. -/// Note that this restricts the size of [`IndexedMedia::content`], which -/// ultimately restricts the size of [`Media::content`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain [`IndexedMediaContentSize`] values when used in conjunction with -/// [`INDEXED_KEY_LOWER_MEDIA_CONTENT_SIZE`]. -const INDEXED_KEY_UPPER_MEDIA_CONTENT_SIZE: IndexedMediaContentSize = - js_sys::Number::MAX_SAFE_INTEGER as usize; - -/// The minimum possible [`Duration`]. -/// -/// This value is useful for constructing a key range over all keys which -/// contain time-related values when used in conjunction with -/// [`INDEXED_KEY_UPPER_DURATION`]. -const INDEXED_KEY_LOWER_DURATION: Duration = Duration::ZERO; - -/// A [`Duration`] constructed with [`js_sys::Number::MAX_SAFE_INTEGER`] -/// seconds. -/// -/// This value is useful for constructing a key range over all keys which -/// contain time-related values in seconds when used in conjunction with -/// [`INDEXED_KEY_LOWER_DURATION`]. -const INDEXED_KEY_UPPER_DURATION_SECONDS: Duration = - Duration::from_secs(js_sys::Number::MAX_SAFE_INTEGER as u64); - -/// Representation of a range of keys of type `K`. This is loosely -/// correlated with [IDBKeyRange][1], with a few differences. -/// -/// Namely, this enum only provides a single way to express a bounded range -/// which is always inclusive on both bounds. While all ranges can still be -/// represented, [`IDBKeyRange`][1] provides more flexibility in this regard. -/// -/// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange -#[derive(Debug, Copy, Clone)] -pub enum IndexedKeyRange { - /// Represents a single key of type `K`. - /// - /// Identical to [`IDBKeyRange.only`][1]. - /// - /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/only - Only(K), - /// Represents an inclusive range of keys of type `K` - /// where the first item is the lower bound and the - /// second item is the upper bound. - /// - /// Similar to [`IDBKeyRange.bound`][1]. - /// - /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/bound - Bound(K, K), -} - -impl<'a, C: 'a> IndexedKeyRange { - /// Encodes a range of key components of type `K::KeyComponents` - /// into a range of keys of type `K`. - pub fn encoded(self, serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedKey = C>, - { - match self { - Self::Only(components) => IndexedKeyRange::Only(K::encode(components, serializer)), - Self::Bound(lower, upper) => { - IndexedKeyRange::Bound(K::encode(lower, serializer), K::encode(upper, serializer)) - } - } - } -} - -impl IndexedKeyRange { - pub fn map(self, f: F) -> IndexedKeyRange - where - F: Fn(K) -> T, - { - match self { - IndexedKeyRange::Only(key) => IndexedKeyRange::Only(f(key)), - IndexedKeyRange::Bound(lower, upper) => IndexedKeyRange::Bound(f(lower), f(upper)), - } - } - - pub fn all(serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedKeyBounds, - { - IndexedKeyRange::Bound(K::lower_key(serializer), K::upper_key(serializer)) - } - - pub fn all_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> IndexedKeyRange - where - T: Indexed, - K: IndexedPrefixKeyBounds, - P: Clone, - { - IndexedKeyRange::Bound( - K::lower_key_with_prefix(prefix.clone(), serializer), - K::upper_key_with_prefix(prefix, serializer), - ) - } -} - -impl From<(K, K)> for IndexedKeyRange { - fn from(value: (K, K)) -> Self { - Self::Bound(value.0, value.1) - } -} - -impl From for IndexedKeyRange { - fn from(value: K) -> Self { - Self::Only(value) - } -} - /// 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`]. @@ -248,7 +105,7 @@ impl Indexed for Lease { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(IndexedLease { id: >::encode(&self.key, serializer), @@ -258,7 +115,7 @@ impl Indexed for Lease { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content) } @@ -275,7 +132,7 @@ pub type IndexedLeaseIdKey = String; impl IndexedKey for IndexedLeaseIdKey { type KeyComponents<'a> = &'a str; - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { serializer.encode_key_as_string(keys::LEASES, components) } } @@ -310,7 +167,7 @@ impl Indexed for MediaRetentionPolicy { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(Self::IndexedType { id: >::encode((), serializer), @@ -320,7 +177,7 @@ impl Indexed for MediaRetentionPolicy { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { serializer.maybe_decrypt_value(indexed.content) } @@ -329,7 +186,7 @@ impl Indexed for MediaRetentionPolicy { impl IndexedKey for IndexedCoreIdKey { type KeyComponents<'a> = (); - fn encode(_components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(_components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { serializer.encode_key_as_string(keys::CORE, keys::MEDIA_RETENTION_POLICY_KEY) } } @@ -379,7 +236,7 @@ impl Indexed for Media { fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { let content = rmp_serde::to_vec_named(&serializer.maybe_encrypt_value(&self.content)?)?; Ok(Self::IndexedType { @@ -410,7 +267,7 @@ impl Indexed for Media { fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result { Ok(Self { metadata: serializer.maybe_decrypt_value(indexed.metadata)?, @@ -431,7 +288,7 @@ pub struct IndexedMediaIdKey(String); impl IndexedKey for IndexedMediaIdKey { type KeyComponents<'a> = &'a MediaRequestParameters; - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { Self(serializer.encode_key_as_string(keys::MEDIA, components.unique_key())) } } @@ -448,7 +305,7 @@ pub struct IndexedMediaSourceKey(String); impl IndexedKey for IndexedMediaSourceKey { type KeyComponents<'a> = &'a MediaSource; - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self { + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self { Self(serializer.encode_key_as_string(keys::MEDIA_SOURCE, components.unique_key())) } } @@ -484,7 +341,7 @@ impl IndexedKey for IndexedMediaContentSizeKey { fn encode( (ignore_policy, content_size): Self::KeyComponents<'_>, - _: &IndexeddbSerializer, + _: &SafeEncodeSerializer, ) -> Self { Self(ignore_policy, content_size) } @@ -535,7 +392,7 @@ impl IndexedKey for IndexedMediaLastAccessKey { fn encode( (ignore_policy, last_access): Self::KeyComponents<'_>, - _: &IndexeddbSerializer, + _: &SafeEncodeSerializer, ) -> Self { Self(ignore_policy, last_access.as_secs()) } @@ -589,7 +446,7 @@ impl IndexedKey for IndexedMediaRetentionMetadataKey { fn encode( (ignore_policy, last_access, content_size): Self::KeyComponents<'_>, - _: &IndexeddbSerializer, + _: &SafeEncodeSerializer, ) -> Self { Self(ignore_policy, last_access.as_secs(), content_size) } diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/mod.rs index 00960ac21a0..6c6171cc7d8 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/serializer/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/serializer/mod.rs @@ -12,157 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License -use gloo_utils::format::JsValueSerdeExt; -use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; -use wasm_bindgen::JsValue; -use web_sys::IdbKeyRange; - -use crate::{ - media_store::serializer::{ - traits::{Indexed, IndexedKey}, - types::IndexedKeyRange, - }, - serializer::IndexeddbSerializer, -}; - +pub mod constants; pub mod foreign; -pub mod traits; -pub mod types; - -#[derive(Debug, Error)] -pub enum IndexeddbMediaStoreSerializerError { - #[error("indexing: {0}")] - Indexing(IndexingError), - #[error("serialization: {0}")] - Serialization(#[from] serde_json::Error), -} - -impl From for IndexeddbMediaStoreSerializerError { - fn from(e: serde_wasm_bindgen::Error) -> Self { - Self::Serialization(serde::de::Error::custom(e.to_string())) - } -} - -/// A (de)serializer for an IndexedDB implementation of [`MediaStore`][1]. -/// -/// This is primarily a wrapper around [`IndexeddbSerializer`] with a -/// convenience functions for (de)serializing types specific to the -/// [`MediaStore`][1]. -/// -/// [1]: matrix_sdk_base::media::store::MediaStore -#[derive(Debug, Clone)] -pub struct IndexeddbMediaStoreSerializer { - inner: IndexeddbSerializer, -} - -impl IndexeddbMediaStoreSerializer { - pub fn new(inner: IndexeddbSerializer) -> Self { - Self { inner } - } - - /// Returns a reference to the inner [`IndexeddbSerializer`]. - pub fn inner(&self) -> &IndexeddbSerializer { - &self.inner - } - - /// Encodes an key for a [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key(&self, components: K::KeyComponents<'_>) -> K - where - T: Indexed, - K: IndexedKey, - { - K::encode(components, &self.inner) - } - - /// Encodes a key for a [`Indexed`] type as a [`JsValue`]. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_as_value( - &self, - components: K::KeyComponents<'_>, - ) -> Result - where - T: Indexed, - K: IndexedKey + Serialize, - { - serde_wasm_bindgen::to_value(&self.encode_key::(components)) - } - - /// Encodes a key component range for an [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_range( - &self, - range: impl Into>, - ) -> Result - where - T: Indexed, - K: Serialize, - { - use serde_wasm_bindgen::to_value; - Ok(match range.into() { - IndexedKeyRange::Only(key) => IdbKeyRange::only(&to_value(&key)?)?, - IndexedKeyRange::Bound(lower, upper) => { - IdbKeyRange::bound(&to_value(&lower)?, &to_value(&upper)?)? - } - }) - } - - /// Encodes a key component range for an [`Indexed`] type. - /// - /// Note that the particular key which is encoded is defined by the type - /// `K`. - pub fn encode_key_component_range<'a, T, K>( - &self, - range: impl Into>>, - ) -> Result - where - T: Indexed, - K: IndexedKey + Serialize, - { - let range = match range.into() { - IndexedKeyRange::Only(components) => { - IndexedKeyRange::Only(K::encode(components, &self.inner)) - } - IndexedKeyRange::Bound(lower, upper) => { - let lower = K::encode(lower, &self.inner); - let upper = K::encode(upper, &self.inner); - IndexedKeyRange::Bound(lower, upper) - } - }; - self.encode_key_range::(range) - } - - /// Serializes an [`Indexed`] type into a [`JsValue`] - pub fn serialize( - &self, - t: &T, - ) -> Result> - where - T: Indexed, - T::IndexedType: Serialize, - { - let indexed = - t.to_indexed(&self.inner).map_err(IndexeddbMediaStoreSerializerError::Indexing)?; - serde_wasm_bindgen::to_value(&indexed).map_err(Into::into) - } - - /// Deserializes an [`Indexed`] type from a [`JsValue`] - pub fn deserialize( - &self, - value: JsValue, - ) -> Result> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - { - let indexed: T::IndexedType = value.into_serde()?; - T::from_indexed(indexed, &self.inner).map_err(IndexeddbMediaStoreSerializerError::Indexing) - } -} +pub mod indexed_types; diff --git a/crates/matrix-sdk-indexeddb/src/media_store/serializer/traits.rs b/crates/matrix-sdk-indexeddb/src/media_store/serializer/traits.rs deleted file mode 100644 index 6b415eb265d..00000000000 --- a/crates/matrix-sdk-indexeddb/src/media_store/serializer/traits.rs +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2025 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License - -use crate::serializer::IndexeddbSerializer; - -/// A conversion trait for preparing high-level types into indexed types -/// which are better suited for storage in IndexedDB. -/// -/// Note that the functions below take an [`IndexeddbSerializer`] as an -/// argument, which provides the necessary context for encryption and -/// decryption, in the case the high-level type must be encrypted before -/// storage. -pub trait Indexed: Sized { - /// The name of the object store in IndexedDB. - const OBJECT_STORE: &'static str; - - /// The indexed type that is used for storage in IndexedDB. - type IndexedType; - /// The error type that is returned when conversion fails. - type Error; - - /// Converts the high-level type into an indexed type. - fn to_indexed( - &self, - serializer: &IndexeddbSerializer, - ) -> Result; - - /// Converts an indexed type into the high-level type. - fn from_indexed( - indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, - ) -> Result; -} - -/// A trait for encoding types which will be used as keys in IndexedDB. -/// -/// Each implementation represents a key on an [`Indexed`] type. -pub trait IndexedKey { - /// The index name for the key, if it represents an index. - const INDEX: Option<&'static str> = None; - - /// Any extra data used to construct the key. - type KeyComponents<'a>; - - /// Encodes the key components into a type that can be used as a key in - /// IndexedDB. - /// - /// Note that this function takes an [`IndexeddbSerializer`] as an - /// argument, which provides the necessary context for encryption and - /// decryption, in the case that certain components of the key must be - /// encrypted before storage. - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self; -} - -/// A trait for constructing the bounds of an [`IndexedKey`]. -/// -/// This is useful when constructing range queries in IndexedDB. -/// -/// The [`IndexedKeyComponentBounds`] helps to specify the upper and lower -/// bounds of the components that are used to create the final key, while the -/// `IndexedKeyBounds` are the upper and lower bounds of the final key itself. -/// -/// While these concepts are similar and often produce the same results, there -/// are cases where these two concepts produce very different results. Namely, -/// when any of the components are encrypted in the process of constructing the -/// final key, then the component bounds and the key bounds produce very -/// different results. -/// -/// So, for instance, consider the `EventId`, which may be encrypted before -/// being used in a final key. One cannot construct the upper and lower bounds -/// of the final key using the upper and lower bounds of the `EventId`, because -/// once the `EventId` is encrypted, the resultant value will no longer express -/// the proper bound. -pub trait IndexedKeyBounds: IndexedKey { - /// Constructs the lower bound of the key. - fn lower_key(serializer: &IndexeddbSerializer) -> Self; - - /// Constructs the upper bound of the key. - fn upper_key(serializer: &IndexeddbSerializer) -> Self; -} - -impl IndexedKeyBounds for K -where - T: Indexed, - K: IndexedKeyComponentBounds + Sized, -{ - /// Constructs the lower bound of the key. - fn lower_key(serializer: &IndexeddbSerializer) -> Self { - >::encode(Self::lower_key_components(), serializer) - } - - /// Constructs the upper bound of the key. - fn upper_key(serializer: &IndexeddbSerializer) -> Self { - >::encode(Self::upper_key_components(), serializer) - } -} - -/// A trait for constructing the bounds of the components of an [`IndexedKey`]. -/// -/// This is useful when constructing range queries in IndexedDB. Note that this -/// trait should not be implemented for key components that are going to be -/// encrypted as ordering properties will not be preserved. -/// -/// One may be interested to read the documentation of [`IndexedKeyBounds`] to -/// get a better overview of how these two interact. -pub trait IndexedKeyComponentBounds: IndexedKeyBounds { - /// Constructs the lower bound of the key components. - fn lower_key_components() -> Self::KeyComponents<'static>; - - /// Constructs the upper bound of the key components. - fn upper_key_components() -> Self::KeyComponents<'static>; -} - -/// A trait for constructing the bounds of an [`IndexedKey`] given a prefix `P` -/// of that key. -/// -/// The key bounds should be constructed by keeping the prefix constant while -/// the remaining components of the key are set to their lower and upper limits. -/// -/// This is useful when constructing prefixed range queries in IndexedDB. -/// -/// Note that the [`IndexedPrefixKeyComponentBounds`] helps to specify the upper -/// and lower bounds of the components that are used to create the final key, -/// while the `IndexedPrefixKeyBounds` are the upper and lower bounds of the -/// final key itself. -/// -/// For details on the differences between key bounds and key component bounds, -/// see the documentation on [`IndexedKeyBounds`]. -pub trait IndexedPrefixKeyBounds: IndexedKey { - /// Constructs the lower bound of the key while maintaining a constant - /// prefix. - fn lower_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self; - - /// Constructs the upper bound of the key while maintaining a constant - /// prefix. - fn upper_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self; -} - -impl<'a, T, K, P> IndexedPrefixKeyBounds for K -where - T: Indexed, - K: IndexedPrefixKeyComponentBounds<'a, T, P> + Sized, - P: 'a, -{ - fn lower_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self { - >::encode(Self::lower_key_components_with_prefix(prefix), serializer) - } - - fn upper_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self { - >::encode(Self::upper_key_components_with_prefix(prefix), serializer) - } -} - -/// A trait for constructing the bounds of the components of an [`IndexedKey`] -/// given a prefix `P` of that key. -/// -/// The key component bounds should be constructed by keeping the prefix -/// constant while the remaining components of the key are set to their lower -/// and upper limits. -/// -/// This is useful when constructing range queries in IndexedDB. -/// -/// Note that this trait should not be implemented for key components that are -/// going to be encrypted as ordering properties will not be preserved. -/// -/// One may be interested to read the documentation of [`IndexedKeyBounds`] to -/// get a better overview of how these two interact. -pub trait IndexedPrefixKeyComponentBounds<'a, T: Indexed, P: 'a>: IndexedKey { - /// Constructs the lower bound of the key components while maintaining a - /// constant prefix. - fn lower_key_components_with_prefix(prefix: P) -> Self::KeyComponents<'a>; - - /// Constructs the upper bound of the key components while maintaining a - /// constant prefix. - fn upper_key_components_with_prefix(prefix: P) -> Self::KeyComponents<'a>; -} diff --git a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs index e4891ba681d..d85747f2194 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/transaction.rs @@ -12,361 +12,68 @@ // See the License for the specific language governing permissions and // limitations under the License -use indexed_db_futures::{prelude::IdbTransaction, IdbQuerySource}; -use matrix_sdk_base::media::store::{MediaRetentionPolicy, MediaStoreError}; -use serde::{ - de::{DeserializeOwned, Error}, - Serialize, -}; -use thiserror::Error; -use web_sys::IdbCursorDirection; +use std::ops::Deref; + +use indexed_db_futures::prelude::IdbTransaction; +use matrix_sdk_base::media::store::MediaRetentionPolicy; -use crate::media_store::{ - error::AsyncErrorDeps, - serializer::{ - traits::{Indexed, IndexedKey}, - types::{IndexedCoreIdKey, IndexedKeyRange, IndexedLeaseIdKey}, - IndexeddbMediaStoreSerializer, +use crate::{ + media_store::{ + serializer::indexed_types::{IndexedCoreIdKey, IndexedLeaseIdKey}, + types::Lease, }, - types::Lease, + serializer::IndexedTypeSerializer, + transaction::{Transaction, TransactionError}, }; -#[derive(Debug, Error)] -pub enum IndexeddbMediaStoreTransactionError { - #[error("DomException {name} ({code}): {message}")] - DomException { name: String, message: String, code: u16 }, - #[error("serialization: {0}")] - Serialization(Box), - #[error("item is not unique")] - ItemIsNotUnique, - #[error("item not found")] - ItemNotFound, -} - -impl From for IndexeddbMediaStoreTransactionError { - fn from(value: web_sys::DomException) -> Self { - Self::DomException { name: value.name(), message: value.message(), code: value.code() } - } -} - -impl From for IndexeddbMediaStoreTransactionError { - fn from(e: serde_wasm_bindgen::Error) -> Self { - Self::Serialization(Box::new(serde_json::Error::custom(e.to_string()))) - } -} - -impl From for MediaStoreError { - fn from(value: IndexeddbMediaStoreTransactionError) -> Self { - use IndexeddbMediaStoreTransactionError::*; - - match value { - DomException { .. } => Self::InvalidData { details: value.to_string() }, - Serialization(e) => Self::Serialization(serde_json::Error::custom(e.to_string())), - ItemIsNotUnique | ItemNotFound => Self::InvalidData { details: value.to_string() }, - } - } -} - /// Represents an IndexedDB transaction, but provides a convenient interface for /// performing operations relevant to the IndexedDB implementation of /// [`MediaStore`](matrix_sdk_base::media::store::MediaStore). pub struct IndexeddbMediaStoreTransaction<'a> { - transaction: IdbTransaction<'a>, - serializer: &'a IndexeddbMediaStoreSerializer, + transaction: Transaction<'a>, } -impl<'a> IndexeddbMediaStoreTransaction<'a> { - pub fn new( - transaction: IdbTransaction<'a>, - serializer: &'a IndexeddbMediaStoreSerializer, - ) -> Self { - Self { transaction, serializer } - } - - /// Returns the underlying IndexedDB transaction. - pub fn into_inner(self) -> IdbTransaction<'a> { - self.transaction - } - - /// Commit all operations tracked in this transaction to IndexedDB. - pub async fn commit(self) -> Result<(), IndexeddbMediaStoreTransactionError> { - self.transaction.await.into_result().map_err(Into::into) - } - - /// Query IndexedDB for items that match the given key range - pub async fn get_items_by_key( - &self, - range: impl Into>, - ) -> Result, IndexeddbMediaStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - let array = if let Some(index) = K::INDEX { - object_store.index(index)?.get_all_with_key(&range)?.await? - } else { - object_store.get_all_with_key(&range)?.await? - }; - let mut items = Vec::with_capacity(array.length() as usize); - for value in array { - let item = self - .serializer - .deserialize(value) - .map_err(|e| IndexeddbMediaStoreTransactionError::Serialization(Box::new(e)))?; - items.push(item); - } - Ok(items) - } - - /// Query IndexedDB for items that match the given key component range - pub async fn get_items_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result, IndexeddbMediaStoreTransactionError> - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.get_items_by_key::(range).await - } +impl<'a> Deref for IndexeddbMediaStoreTransaction<'a> { + type Target = Transaction<'a>; - /// Query IndexedDB for items that match the given key. If - /// more than one item is found, an error is returned. - pub async fn get_item_by_key( - &self, - key: K, - ) -> Result, IndexeddbMediaStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let mut items = self.get_items_by_key::(key).await?; - if items.len() > 1 { - return Err(IndexeddbMediaStoreTransactionError::ItemIsNotUnique); - } - Ok(items.pop()) - } - - /// Query IndexedDB for items that match the given key components. If more - /// than one item is found, an error is returned. - pub async fn get_item_by_key_components<'b, T, K>( - &self, - components: K::KeyComponents<'b>, - ) -> Result, IndexeddbMediaStoreTransactionError> - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let mut items = self.get_items_by_key_components::(components).await?; - if items.len() > 1 { - return Err(IndexeddbMediaStoreTransactionError::ItemIsNotUnique); - } - Ok(items.pop()) - } - - /// Query IndexedDB for the number of items that match the given key range. - pub async fn get_items_count_by_key( - &self, - range: impl Into>, - ) -> Result - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - let count = if let Some(index) = K::INDEX { - object_store.index(index)?.count_with_key(&range)?.await? - } else { - object_store.count_with_key(&range)?.await? - }; - Ok(count as usize) - } - - /// Query IndexedDB for the number of items that match the given key - /// components range. - pub async fn get_items_count_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result - where - T: Indexed + 'b, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.get_items_count_by_key::(range).await + fn deref(&self) -> &Self::Target { + &self.transaction } +} - /// Query IndexedDB for the item with the maximum key in the given range. - pub async fn get_max_item_by_key( - &self, - range: impl Into>, - ) -> Result, IndexeddbMediaStoreTransactionError> - where - T: Indexed, - T::IndexedType: DeserializeOwned, - T::Error: AsyncErrorDeps, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let direction = IdbCursorDirection::Prev; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - if let Some(index) = K::INDEX { - object_store - .index(index)? - .open_cursor_with_range_and_direction(&range, direction)? - .await? - .map(|cursor| self.serializer.deserialize(cursor.value())) - .transpose() - .map_err(|e| IndexeddbMediaStoreTransactionError::Serialization(Box::new(e))) - } else { - object_store - .open_cursor_with_range_and_direction(&range, direction)? - .await? - .map(|cursor| self.serializer.deserialize(cursor.value())) - .transpose() - .map_err(|e| IndexeddbMediaStoreTransactionError::Serialization(Box::new(e))) - } +impl<'a> IndexeddbMediaStoreTransaction<'a> { + pub fn new(transaction: IdbTransaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self { + Self { transaction: Transaction::new(transaction, serializer) } } - /// Adds an item to the corresponding IndexedDB object - /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already - /// exists, it will be rejected. - pub async fn add_item(&self, item: &T) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed + Serialize, - T::IndexedType: Serialize, - T::Error: AsyncErrorDeps, - { + /// Returns the underlying IndexedDB transaction. + pub fn into_inner(self) -> Transaction<'a> { self.transaction - .object_store(T::OBJECT_STORE)? - .add_val_owned( - self.serializer - .serialize(item) - .map_err(|e| IndexeddbMediaStoreTransactionError::Serialization(Box::new(e)))?, - )? - .await - .map_err(Into::into) } - /// Puts an item in the corresponding IndexedDB object - /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already - /// exists, it will be overwritten. - pub async fn put_item(&self, item: &T) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed + Serialize, - T::IndexedType: Serialize, - T::Error: AsyncErrorDeps, - { - self.transaction - .object_store(T::OBJECT_STORE)? - .put_val_owned( - self.serializer - .serialize(item) - .map_err(|e| IndexeddbMediaStoreTransactionError::Serialization(Box::new(e)))?, - )? - .await - .map_err(Into::into) - } - - /// Delete items in given key range from IndexedDB - pub async fn delete_items_by_key( - &self, - range: impl Into>, - ) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed, - K: IndexedKey + Serialize, - { - let range = self.serializer.encode_key_range::(range)?; - let object_store = self.transaction.object_store(T::OBJECT_STORE)?; - if let Some(index) = K::INDEX { - let index = object_store.index(index)?; - if let Some(cursor) = index.open_cursor_with_range(&range)?.await? { - while cursor.key().is_some() { - cursor.delete()?.await?; - cursor.continue_cursor()?.await?; - } - } - } else { - object_store.delete_owned(&range)?.await?; - } - Ok(()) - } - - /// Delete items in the given key component range from - /// IndexedDB - pub async fn delete_items_by_key_components<'b, T, K>( - &self, - range: impl Into>>, - ) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed + 'b, - K: IndexedKey + Serialize + 'b, - { - let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); - self.delete_items_by_key::(range).await - } - - /// Delete item that matches the given key components from - /// IndexedDB - pub async fn delete_item_by_key<'b, T, K>( - &self, - key: K::KeyComponents<'b>, - ) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed + 'b, - K: IndexedKey + Serialize + 'b, - { - self.delete_items_by_key_components::(key).await - } - - /// Clear all items of type `T` from the associated object store - /// `T::OBJECT_STORE` from IndexedDB - pub async fn clear(&self) -> Result<(), IndexeddbMediaStoreTransactionError> - where - T: Indexed, - { - self.transaction.object_store(T::OBJECT_STORE)?.clear()?.await.map_err(Into::into) + /// Commit all operations tracked in this transaction to IndexedDB. + pub async fn commit(self) -> Result<(), TransactionError> { + self.transaction.commit().await } /// Query IndexedDB for the lease that matches the given key `id`. If more /// than one lease is found, an error is returned. - pub async fn get_lease_by_id( - &self, - id: &str, - ) -> Result, IndexeddbMediaStoreTransactionError> { - self.get_item_by_key_components::(id).await + pub async fn get_lease_by_id(&self, id: &str) -> Result, TransactionError> { + self.transaction.get_item_by_key_components::(id).await } /// Puts a lease into IndexedDB. If a media with the same key already /// exists, it will be overwritten. - pub async fn put_lease( - &self, - lease: &Lease, - ) -> Result<(), IndexeddbMediaStoreTransactionError> { - self.put_item(lease).await + pub async fn put_lease(&self, lease: &Lease) -> Result<(), TransactionError> { + self.transaction.put_item(lease).await } /// Query IndexedDB for the stored [`MediaRetentionPolicy`] pub async fn get_media_retention_policy( &self, - ) -> Result, IndexeddbMediaStoreTransactionError> { - self.get_item_by_key_components::(()).await + ) -> Result, TransactionError> { + self.transaction + .get_item_by_key_components::(()) + .await } } diff --git a/crates/matrix-sdk-indexeddb/src/serialize_bool_for_indexeddb.rs b/crates/matrix-sdk-indexeddb/src/serialize_bool_for_indexeddb.rs deleted file mode 100644 index be092b1f2a5..00000000000 --- a/crates/matrix-sdk-indexeddb/src/serialize_bool_for_indexeddb.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Booleans don't work as keys in indexeddb (see [ECMA spec]), so instead we -//! serialize them as `0` or `1`. -//! -//! This module implements a custom serializer which can be used on `bool` -//! struct fields with: -//! -//! ```ignore -//! #[serde(with = "serialize_bool_for_indexeddb")] -//! ``` -//! -//! [ECMA spec]: https://w3c.github.io/IndexedDB/#key -use serde::{Deserializer, Serializer}; - -pub fn serialize(v: &bool, s: S) -> Result -where - S: Serializer, -{ - s.serialize_u8(if *v { 1 } else { 0 }) -} - -pub fn deserialize<'de, D>(d: D) -> Result -where - D: Deserializer<'de>, -{ - let v: u8 = serde::de::Deserialize::deserialize(d)?; - Ok(v != 0) -} diff --git a/crates/matrix-sdk-indexeddb/src/serializer/foreign.rs b/crates/matrix-sdk-indexeddb/src/serializer/foreign.rs new file mode 100644 index 00000000000..10c2107c07a --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/foreign.rs @@ -0,0 +1,43 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod bool { + //! Booleans don't work as keys in indexeddb (see [ECMA spec]), so instead + //! we serialize them as `0` or `1`. + //! + //! This module implements a custom serializer which can be used on `bool` + //! struct fields with: + //! + //! ```ignore + //! #[serde(with = "crate::serializer::foreign::bool")] + //! ``` + //! + //! [ECMA spec]: https://w3c.github.io/IndexedDB/#key + use serde::{Deserializer, Serializer}; + + pub fn serialize(v: &bool, s: S) -> Result + where + S: Serializer, + { + s.serialize_u8(if *v { 1 } else { 0 }) + } + + pub fn deserialize<'de, D>(d: D) -> Result + where + D: Deserializer<'de>, + { + let v: u8 = serde::de::Deserialize::deserialize(d)?; + Ok(v != 0) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/constants.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/constants.rs new file mode 100644 index 00000000000..2a26fddd6fd --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/constants.rs @@ -0,0 +1,57 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +use std::{sync::LazyLock, time::Duration}; + +/// The first unicode character, and hence the lower bound for IndexedDB keys +/// (or key components) which are represented as strings. +/// +/// This value is useful for constructing a key range over all strings when used +/// in conjunction with [`INDEXED_KEY_UPPER_CHARACTER`]. +pub const INDEXED_KEY_LOWER_CHARACTER: char = '\u{0000}'; + +/// The last unicode character in the [Basic Multilingual Plane][1]. This seems +/// like a reasonable place to set the upper bound for IndexedDB keys (or key +/// components) which are represented as strings, though one could +/// theoretically set it to `\u{10FFFF}`. +/// +/// This value is useful for constructing a key range over all strings when used +/// in conjunction with [`INDEXED_KEY_LOWER_CHARACTER`]. +/// +/// [1]: https://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane +pub const INDEXED_KEY_UPPER_CHARACTER: char = '\u{FFFF}'; + +/// Identical to [`INDEXED_KEY_LOWER_CHARACTER`] but represented as a [`String`] +pub static INDEXED_KEY_LOWER_STRING: LazyLock = + LazyLock::new(|| String::from(INDEXED_KEY_LOWER_CHARACTER)); + +/// Identical to [`INDEXED_KEY_UPPER_CHARACTER`] but represented as a [`String`] +pub static INDEXED_KEY_UPPER_STRING: LazyLock = + LazyLock::new(|| String::from(INDEXED_KEY_UPPER_CHARACTER)); + +/// The minimum possible [`Duration`]. +/// +/// This value is useful for constructing a key range over all keys which +/// contain time-related values when used in conjunction with +/// [`INDEXED_KEY_UPPER_DURATION`]. +pub const INDEXED_KEY_LOWER_DURATION: Duration = Duration::ZERO; + +/// A [`Duration`] constructed with [`js_sys::Number::MAX_SAFE_INTEGER`] +/// seconds. +/// +/// This value is useful for constructing a key range over all keys which +/// contain time-related values in seconds when used in conjunction with +/// [`INDEXED_KEY_LOWER_DURATION`]. +pub const INDEXED_KEY_UPPER_DURATION_SECONDS: Duration = + Duration::from_secs(js_sys::Number::MAX_SAFE_INTEGER as u64); diff --git a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs new file mode 100644 index 00000000000..a91af1c89d2 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/mod.rs @@ -0,0 +1,163 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +// Allow dead code here, as this module is still in the process +// of being developed, so some functions will be used later on. +// Once development is complete, we can remove this line and +// clean up any dead code. +#![allow(dead_code)] + +pub mod constants; +pub mod range; +pub mod traits; + +use gloo_utils::format::JsValueSerdeExt; +use range::IndexedKeyRange; +use serde::{de::DeserializeOwned, Serialize}; +use thiserror::Error; +use traits::{Indexed, IndexedKey}; +use wasm_bindgen::JsValue; +use web_sys::IdbKeyRange; + +use crate::serializer::SafeEncodeSerializer; + +#[derive(Debug, Error)] +pub enum IndexedTypeSerializerError { + #[error("indexing: {0}")] + Indexing(IndexingError), + #[error("serialization: {0}")] + Serialization(#[from] serde_json::Error), +} + +impl From for IndexedTypeSerializerError { + fn from(e: serde_wasm_bindgen::Error) -> Self { + Self::Serialization(serde::de::Error::custom(e.to_string())) + } +} + +/// A (de)serializer for an IndexedDB implementation of [`EventCacheStore`][1]. +/// +/// This is primarily a wrapper around [`SafeEncodeSerializer`] with +/// convenience functions for (de)serializing types specific to the +/// [`EventCacheStore`][1]. +/// +/// [1]: matrix_sdk_base::event_cache::store::EventCacheStore +#[derive(Debug, Clone)] +pub struct IndexedTypeSerializer { + inner: SafeEncodeSerializer, +} + +impl IndexedTypeSerializer { + pub fn new(inner: SafeEncodeSerializer) -> Self { + Self { inner } + } + + /// Returns a reference to the inner [`IndexeddbSerializer`]. + pub fn inner(&self) -> &SafeEncodeSerializer { + &self.inner + } + + /// Encodes an key for a [`Indexed`] type. + /// + /// Note that the particular key which is encoded is defined by the type + /// `K`. + pub fn encode_key(&self, components: K::KeyComponents<'_>) -> K + where + T: Indexed, + K: IndexedKey, + { + K::encode(components, &self.inner) + } + + /// Encodes a key for a [`Indexed`] type as a [`JsValue`]. + /// + /// Note that the particular key which is encoded is defined by the type + /// `K`. + pub fn encode_key_as_value( + &self, + components: K::KeyComponents<'_>, + ) -> Result + where + T: Indexed, + K: IndexedKey + Serialize, + { + serde_wasm_bindgen::to_value(&self.encode_key::(components)) + } + + /// Encodes a key component range for an [`Indexed`] type. + /// + /// Note that the particular key which is encoded is defined by the type + /// `K`. + pub fn encode_key_range( + &self, + range: impl Into>, + ) -> Result + where + T: Indexed, + K: Serialize, + { + use serde_wasm_bindgen::to_value; + Ok(match range.into() { + IndexedKeyRange::Only(key) => IdbKeyRange::only(&to_value(&key)?)?, + IndexedKeyRange::Bound(lower, upper) => { + IdbKeyRange::bound(&to_value(&lower)?, &to_value(&upper)?)? + } + }) + } + + /// Encodes a key component range for an [`Indexed`] type. + /// + /// Note that the particular key which is encoded is defined by the type + /// `K`. + pub fn encode_key_component_range<'a, T, K>( + &self, + range: impl Into>>, + ) -> Result + where + T: Indexed, + K: IndexedKey + Serialize, + { + let range = match range.into() { + IndexedKeyRange::Only(components) => { + IndexedKeyRange::Only(K::encode(components, &self.inner)) + } + IndexedKeyRange::Bound(lower, upper) => { + let lower = K::encode(lower, &self.inner); + let upper = K::encode(upper, &self.inner); + IndexedKeyRange::Bound(lower, upper) + } + }; + self.encode_key_range::(range) + } + + /// Serializes an [`Indexed`] type into a [`JsValue`] + pub fn serialize(&self, t: &T) -> Result> + where + T: Indexed, + T::IndexedType: Serialize, + { + let indexed = t.to_indexed(&self.inner).map_err(IndexedTypeSerializerError::Indexing)?; + serde_wasm_bindgen::to_value(&indexed).map_err(Into::into) + } + + /// Deserializes an [`Indexed`] type from a [`JsValue`] + pub fn deserialize(&self, value: JsValue) -> Result> + where + T: Indexed, + T::IndexedType: DeserializeOwned, + { + let indexed: T::IndexedType = value.into_serde()?; + T::from_indexed(indexed, &self.inner).map_err(IndexedTypeSerializerError::Indexing) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/range.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/range.rs new file mode 100644 index 00000000000..bd2d748519a --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/range.rs @@ -0,0 +1,104 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +use crate::serializer::{ + Indexed, IndexedKey, IndexedKeyBounds, IndexedPrefixKeyBounds, SafeEncodeSerializer, +}; + +/// Representation of a range of keys of type `K`. This is loosely +/// correlated with [IDBKeyRange][1], with a few differences. +/// +/// Namely, this enum only provides a single way to express a bounded range +/// which is always inclusive on both bounds. While all ranges can still be +/// represented, [`IDBKeyRange`][1] provides more flexibility in this regard. +/// +/// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange +#[derive(Debug, Copy, Clone)] +pub enum IndexedKeyRange { + /// Represents a single key of type `K`. + /// + /// Identical to [`IDBKeyRange.only`][1]. + /// + /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/only + Only(K), + /// Represents an inclusive range of keys of type `K` + /// where the first item is the lower bound and the + /// second item is the upper bound. + /// + /// Similar to [`IDBKeyRange.bound`][1]. + /// + /// [1]: https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange/bound + Bound(K, K), +} + +impl<'a, C: 'a> IndexedKeyRange { + /// Encodes a range of key components of type `K::KeyComponents` + /// into a range of keys of type `K`. + pub fn encoded(self, serializer: &SafeEncodeSerializer) -> IndexedKeyRange + where + T: Indexed, + K: IndexedKey = C>, + { + match self { + Self::Only(components) => IndexedKeyRange::Only(K::encode(components, serializer)), + Self::Bound(lower, upper) => { + IndexedKeyRange::Bound(K::encode(lower, serializer), K::encode(upper, serializer)) + } + } + } +} + +impl IndexedKeyRange { + pub fn map(self, f: F) -> IndexedKeyRange + where + F: Fn(K) -> T, + { + match self { + IndexedKeyRange::Only(key) => IndexedKeyRange::Only(f(key)), + IndexedKeyRange::Bound(lower, upper) => IndexedKeyRange::Bound(f(lower), f(upper)), + } + } + + pub fn all(serializer: &SafeEncodeSerializer) -> IndexedKeyRange + where + T: Indexed, + K: IndexedKeyBounds, + { + IndexedKeyRange::Bound(K::lower_key(serializer), K::upper_key(serializer)) + } + + pub fn all_with_prefix(prefix: P, serializer: &SafeEncodeSerializer) -> IndexedKeyRange + where + T: Indexed, + K: IndexedPrefixKeyBounds, + P: Clone, + { + IndexedKeyRange::Bound( + K::lower_key_with_prefix(prefix.clone(), serializer), + K::upper_key_with_prefix(prefix, serializer), + ) + } +} + +impl From<(K, K)> for IndexedKeyRange { + fn from(value: (K, K)) -> Self { + Self::Bound(value.0, value.1) + } +} + +impl From for IndexedKeyRange { + fn from(value: K) -> Self { + Self::Only(value) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/traits.rs similarity index 90% rename from crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs rename to crates/matrix-sdk-indexeddb/src/serializer/indexed_type/traits.rs index 6b415eb265d..a974431087c 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/indexed_type/traits.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License -use crate::serializer::IndexeddbSerializer; +use crate::serializer::SafeEncodeSerializer; /// A conversion trait for preparing high-level types into indexed types /// which are better suited for storage in IndexedDB. @@ -33,13 +33,13 @@ pub trait Indexed: Sized { /// Converts the high-level type into an indexed type. fn to_indexed( &self, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result; /// Converts an indexed type into the high-level type. fn from_indexed( indexed: Self::IndexedType, - serializer: &IndexeddbSerializer, + serializer: &SafeEncodeSerializer, ) -> Result; } @@ -60,7 +60,7 @@ pub trait IndexedKey { /// argument, which provides the necessary context for encryption and /// decryption, in the case that certain components of the key must be /// encrypted before storage. - fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self; + fn encode(components: Self::KeyComponents<'_>, serializer: &SafeEncodeSerializer) -> Self; } /// A trait for constructing the bounds of an [`IndexedKey`]. @@ -84,10 +84,10 @@ pub trait IndexedKey { /// the proper bound. pub trait IndexedKeyBounds: IndexedKey { /// Constructs the lower bound of the key. - fn lower_key(serializer: &IndexeddbSerializer) -> Self; + fn lower_key(serializer: &SafeEncodeSerializer) -> Self; /// Constructs the upper bound of the key. - fn upper_key(serializer: &IndexeddbSerializer) -> Self; + fn upper_key(serializer: &SafeEncodeSerializer) -> Self; } impl IndexedKeyBounds for K @@ -96,12 +96,12 @@ where K: IndexedKeyComponentBounds + Sized, { /// Constructs the lower bound of the key. - fn lower_key(serializer: &IndexeddbSerializer) -> Self { + fn lower_key(serializer: &SafeEncodeSerializer) -> Self { >::encode(Self::lower_key_components(), serializer) } /// Constructs the upper bound of the key. - fn upper_key(serializer: &IndexeddbSerializer) -> Self { + fn upper_key(serializer: &SafeEncodeSerializer) -> Self { >::encode(Self::upper_key_components(), serializer) } } @@ -140,11 +140,11 @@ pub trait IndexedKeyComponentBounds: IndexedKeyBounds { pub trait IndexedPrefixKeyBounds: IndexedKey { /// Constructs the lower bound of the key while maintaining a constant /// prefix. - fn lower_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self; + fn lower_key_with_prefix(prefix: P, serializer: &SafeEncodeSerializer) -> Self; /// Constructs the upper bound of the key while maintaining a constant /// prefix. - fn upper_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self; + fn upper_key_with_prefix(prefix: P, serializer: &SafeEncodeSerializer) -> Self; } impl<'a, T, K, P> IndexedPrefixKeyBounds for K @@ -153,11 +153,11 @@ where K: IndexedPrefixKeyComponentBounds<'a, T, P> + Sized, P: 'a, { - fn lower_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self { + fn lower_key_with_prefix(prefix: P, serializer: &SafeEncodeSerializer) -> Self { >::encode(Self::lower_key_components_with_prefix(prefix), serializer) } - fn upper_key_with_prefix(prefix: P, serializer: &IndexeddbSerializer) -> Self { + fn upper_key_with_prefix(prefix: P, serializer: &SafeEncodeSerializer) -> Self { >::encode(Self::upper_key_components_with_prefix(prefix), serializer) } } diff --git a/crates/matrix-sdk-indexeddb/src/serializer/mod.rs b/crates/matrix-sdk-indexeddb/src/serializer/mod.rs new file mode 100644 index 00000000000..fe2abc0e681 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/mod.rs @@ -0,0 +1,42 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(feature = "e2e-encryption")] +pub mod foreign; + +#[cfg(all( + feature = "e2e-encryption", + any(feature = "event-cache-store", feature = "media-store") +))] +pub mod indexed_type; +#[cfg(all( + feature = "e2e-encryption", + any(feature = "event-cache-store", feature = "media-store") +))] +pub use indexed_type::{ + constants::{ + INDEXED_KEY_LOWER_CHARACTER, INDEXED_KEY_LOWER_DURATION, INDEXED_KEY_LOWER_STRING, + INDEXED_KEY_UPPER_CHARACTER, INDEXED_KEY_UPPER_DURATION_SECONDS, INDEXED_KEY_UPPER_STRING, + }, + range::IndexedKeyRange, + traits::{ + Indexed, IndexedKey, IndexedKeyBounds, IndexedKeyComponentBounds, IndexedPrefixKeyBounds, + IndexedPrefixKeyComponentBounds, + }, + IndexedTypeSerializer, +}; + +pub mod safe_encode; +#[cfg(feature = "e2e-encryption")] +pub use safe_encode::types::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError}; diff --git a/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/mod.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/mod.rs new file mode 100644 index 00000000000..669b2306da8 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod traits; + +#[cfg(feature = "e2e-encryption")] +pub mod types; diff --git a/crates/matrix-sdk-indexeddb/src/safe_encode.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs similarity index 100% rename from crates/matrix-sdk-indexeddb/src/safe_encode.rs rename to crates/matrix-sdk-indexeddb/src/serializer/safe_encode/traits.rs diff --git a/crates/matrix-sdk-indexeddb/src/serializer.rs b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs similarity index 93% rename from crates/matrix-sdk-indexeddb/src/serializer.rs rename to crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs index cb29b1ee77d..c7ab2fb0828 100644 --- a/crates/matrix-sdk-indexeddb/src/serializer.rs +++ b/crates/matrix-sdk-indexeddb/src/serializer/safe_encode/types.rs @@ -27,20 +27,20 @@ use wasm_bindgen::JsValue; use web_sys::IdbKeyRange; use zeroize::Zeroizing; -use crate::safe_encode::SafeEncode; +use crate::serializer::safe_encode::traits::SafeEncode; -type Result = std::result::Result; +type Result = std::result::Result; const BASE64: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD); /// Handles the functionality of serializing and encrypting data for the /// indexeddb store. #[derive(Clone)] -pub struct IndexeddbSerializer { +pub struct SafeEncodeSerializer { store_cipher: Option>, } -impl std::fmt::Debug for IndexeddbSerializer { +impl std::fmt::Debug for SafeEncodeSerializer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("IndexeddbSerializer") .field("store_cipher", &self.store_cipher.as_ref().map(|_| "")) @@ -49,7 +49,7 @@ impl std::fmt::Debug for IndexeddbSerializer { } #[derive(Debug, thiserror::Error)] -pub enum IndexeddbSerializerError { +pub enum SafeEncodeSerializerError { #[error(transparent)] Serialization(#[from] serde_json::Error), #[error("DomException {name} ({code}): {message}")] @@ -65,13 +65,13 @@ pub enum IndexeddbSerializerError { CryptoStoreError(#[from] CryptoStoreError), } -impl From for IndexeddbSerializerError { +impl From for SafeEncodeSerializerError { fn from(frm: web_sys::DomException) -> Self { Self::DomException { name: frm.name(), message: frm.message(), code: frm.code() } } } -impl From for IndexeddbSerializerError { +impl From for SafeEncodeSerializerError { fn from(e: serde_wasm_bindgen::Error) -> Self { Self::Serialization(serde::de::Error::custom(e.to_string())) } @@ -84,7 +84,7 @@ pub enum MaybeEncrypted { Unencrypted(String), } -impl IndexeddbSerializer { +impl SafeEncodeSerializer { pub fn new(store_cipher: Option>) -> Self { Self { store_cipher } } @@ -150,7 +150,7 @@ impl IndexeddbSerializer { &self, table_name: &str, key: T, - ) -> Result + ) -> Result where T: SafeEncode, { @@ -158,7 +158,7 @@ impl IndexeddbSerializer { Some(cipher) => key.encode_to_range_secure(table_name, cipher), None => key.encode_to_range(), } - .map_err(|e| IndexeddbSerializerError::DomException { + .map_err(|e| SafeEncodeSerializerError::DomException { code: 0, name: "IdbKeyRangeMakeError".to_owned(), message: e, @@ -173,7 +173,7 @@ impl IndexeddbSerializer { pub fn serialize_value( &self, value: &impl Serialize, - ) -> Result { + ) -> Result { let serialized = self.maybe_encrypt_value(value)?; Ok(serde_wasm_bindgen::to_value(&serialized)?) } @@ -227,7 +227,7 @@ impl IndexeddbSerializer { pub fn deserialize_value( &self, value: JsValue, - ) -> Result { + ) -> Result { // Objects which are serialized nowadays should be represented as a // `MaybeEncrypted`. However, `serialize_value` previously used a // different format, so we need to handle that in case we have old data. @@ -281,11 +281,11 @@ impl IndexeddbSerializer { pub fn deserialize_legacy_value( &self, value: JsValue, - ) -> Result { + ) -> Result { match &self.store_cipher { Some(cipher) => { if !value.is_array() { - return Err(IndexeddbSerializerError::CryptoStoreError( + return Err(SafeEncodeSerializerError::CryptoStoreError( CryptoStoreError::UnpicklingError, )); } @@ -363,7 +363,7 @@ mod tests { use serde_json::json; use wasm_bindgen::JsValue; - use super::IndexeddbSerializer; + use super::SafeEncodeSerializer; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); @@ -371,7 +371,7 @@ mod tests { /// cipher is in use. #[async_test] async fn test_serialize_deserialize_with_cipher() { - let serializer = IndexeddbSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); + let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); let obj = make_test_object(); let serialized = serializer.serialize_value(&obj).expect("could not serialize"); @@ -385,7 +385,7 @@ mod tests { /// cipher is in use. #[async_test] async fn test_serialize_deserialize_no_cipher() { - let serializer = IndexeddbSerializer::new(None); + let serializer = SafeEncodeSerializer::new(None); let obj = make_test_object(); let serialized = serializer.serialize_value(&obj).expect("could not serialize"); let deserialized: TestStruct = @@ -413,7 +413,7 @@ mod tests { // Now, try deserializing with `deserialize_value`, and check we get the right // thing. - let serializer = IndexeddbSerializer::new(Some(Arc::new(cipher))); + let serializer = SafeEncodeSerializer::new(Some(Arc::new(cipher))); let deserialized: TestStruct = serializer.deserialize_value(serialized).expect("could not deserialize"); @@ -429,7 +429,7 @@ mod tests { let json = json!({ "id":0, "name": "test", "map": { "0": "test" }}); let serialized = js_sys::JSON::parse(&json.to_string()).unwrap(); - let serializer = IndexeddbSerializer::new(None); + let serializer = SafeEncodeSerializer::new(None); let deserialized: TestStruct = serializer.deserialize_value(serialized).expect("could not deserialize"); @@ -444,7 +444,7 @@ mod tests { let json = json!([1, 2, 3, 4]); let serialized = js_sys::JSON::parse(&json.to_string()).unwrap(); - let serializer = IndexeddbSerializer::new(None); + let serializer = SafeEncodeSerializer::new(None); let deserialized: Vec = serializer.deserialize_value(serialized).expect("could not deserialize"); @@ -455,7 +455,7 @@ mod tests { /// `maybe_encrypt_value`, when a cipher is in use. #[async_test] async fn test_maybe_encrypt_deserialize_with_cipher() { - let serializer = IndexeddbSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); + let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); let obj = make_test_object(); let serialized = serializer.maybe_encrypt_value(&obj).expect("could not serialize"); @@ -471,7 +471,7 @@ mod tests { /// `maybe_encrypt_value`, when no cipher is in use. #[async_test] async fn test_maybe_encrypt_deserialize_no_cipher() { - let serializer = IndexeddbSerializer::new(None); + let serializer = SafeEncodeSerializer::new(None); let obj = make_test_object(); let serialized = serializer.maybe_encrypt_value(&obj).expect("could not serialize"); let serialized = serde_wasm_bindgen::to_value(&serialized).unwrap(); @@ -485,7 +485,7 @@ mod tests { /// when a cipher is in use. #[async_test] async fn test_maybe_encrypt_decrypt_with_cipher() { - let serializer = IndexeddbSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); + let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap()))); let obj = make_test_object(); let serialized = serializer.maybe_encrypt_value(&obj).expect("could not serialize"); @@ -499,7 +499,7 @@ mod tests { /// when no cipher is in use. #[async_test] async fn test_maybe_encrypt_decrypt_no_cipher() { - let serializer = IndexeddbSerializer::new(None); + let serializer = SafeEncodeSerializer::new(None); let obj = make_test_object(); let serialized = serializer.maybe_encrypt_value(&obj).expect("could not serialize"); diff --git a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs index cce80e7f075..8eed725ed3a 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/migrations.rs @@ -853,7 +853,7 @@ mod tests { use super::{old_keys, MigrationConflictStrategy, CURRENT_DB_VERSION, CURRENT_META_DB_VERSION}; use crate::{ - safe_encode::SafeEncode, + serializer::safe_encode::traits::SafeEncode, state_store::{encode_key, keys, serialize_value, Result}, IndexeddbStateStore, IndexeddbStateStoreError, }; diff --git a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs index 82728bf17d2..e0eaa2f9437 100644 --- a/crates/matrix-sdk-indexeddb/src/state_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/state_store/mod.rs @@ -59,7 +59,7 @@ mod migrations; pub use self::migrations::MigrationConflictStrategy; use self::migrations::{upgrade_inner_db, upgrade_meta_db}; -use crate::safe_encode::SafeEncode; +use crate::serializer::safe_encode::traits::SafeEncode; #[derive(Debug, thiserror::Error)] pub enum IndexeddbStateStoreError { diff --git a/crates/matrix-sdk-indexeddb/src/transaction/mod.rs b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs new file mode 100644 index 00000000000..fb32abbc0a7 --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/transaction/mod.rs @@ -0,0 +1,335 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +// Allow dead code here, as this module is still in the process +// of being developed, so some functions will be used later on. +// Once development is complete, we can remove this line and +// clean up any dead code. +#![allow(dead_code)] + +use indexed_db_futures::{prelude::IdbTransaction, IdbQuerySource}; +use serde::{ + de::{DeserializeOwned, Error}, + Serialize, +}; +use thiserror::Error; +use web_sys::IdbCursorDirection; + +use crate::{ + error::AsyncErrorDeps, + serializer::{Indexed, IndexedKey, IndexedKeyRange, IndexedTypeSerializer}, +}; + +#[derive(Debug, Error)] +pub enum TransactionError { + #[error("DomException {name} ({code}): {message}")] + DomException { name: String, message: String, code: u16 }, + #[error("serialization: {0}")] + Serialization(Box), + #[error("item is not unique")] + ItemIsNotUnique, + #[error("item not found")] + ItemNotFound, +} + +impl From for TransactionError { + fn from(value: web_sys::DomException) -> Self { + Self::DomException { name: value.name(), message: value.message(), code: value.code() } + } +} + +impl From for TransactionError { + fn from(e: serde_wasm_bindgen::Error) -> Self { + Self::Serialization(Box::new(serde_json::Error::custom(e.to_string()))) + } +} + +/// Represents an IndexedDB transaction, but provides a convenient interface for +/// performing operations on types that implement [`Indexed`] and related +/// traits. +pub struct Transaction<'a> { + transaction: IdbTransaction<'a>, + serializer: &'a IndexedTypeSerializer, +} + +impl<'a> Transaction<'a> { + pub fn new(transaction: IdbTransaction<'a>, serializer: &'a IndexedTypeSerializer) -> Self { + Self { transaction, serializer } + } + + /// Returns the serializer performing (de)serialization for this + /// [`Transaction`] + pub fn serializer(&self) -> &IndexedTypeSerializer { + self.serializer + } + + /// Returns the underlying IndexedDB transaction. + pub fn into_inner(self) -> IdbTransaction<'a> { + self.transaction + } + + /// Commit all operations tracked in this transaction to IndexedDB. + pub async fn commit(self) -> Result<(), TransactionError> { + self.transaction.await.into_result().map_err(Into::into) + } + + /// Query IndexedDB for items that match the given key range + pub async fn get_items_by_key( + &self, + range: impl Into>, + ) -> Result, TransactionError> + where + T: Indexed, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize, + { + let range = self.serializer.encode_key_range::(range)?; + let object_store = self.transaction.object_store(T::OBJECT_STORE)?; + let array = if let Some(index) = K::INDEX { + object_store.index(index)?.get_all_with_key(&range)?.await? + } else { + object_store.get_all_with_key(&range)?.await? + }; + let mut items = Vec::with_capacity(array.length() as usize); + for value in array { + let item = self + .serializer + .deserialize(value) + .map_err(|e| TransactionError::Serialization(Box::new(e)))?; + items.push(item); + } + Ok(items) + } + + /// Query IndexedDB for items that match the given key component range + pub async fn get_items_by_key_components<'b, T, K>( + &self, + range: impl Into>>, + ) -> Result, TransactionError> + where + T: Indexed + 'b, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize + 'b, + { + let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); + self.get_items_by_key::(range).await + } + + /// Query IndexedDB for items that match the given key. If + /// more than one item is found, an error is returned. + pub async fn get_item_by_key(&self, key: K) -> Result, TransactionError> + where + T: Indexed, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize, + { + let mut items = self.get_items_by_key::(key).await?; + if items.len() > 1 { + return Err(TransactionError::ItemIsNotUnique); + } + Ok(items.pop()) + } + + /// Query IndexedDB for items that match the given key components. If more + /// than one item is found, an error is returned. + pub async fn get_item_by_key_components<'b, T, K>( + &self, + components: K::KeyComponents<'b>, + ) -> Result, TransactionError> + where + T: Indexed + 'b, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize + 'b, + { + let mut items = self.get_items_by_key_components::(components).await?; + if items.len() > 1 { + return Err(TransactionError::ItemIsNotUnique); + } + Ok(items.pop()) + } + + /// Query IndexedDB for the number of items that match the given key range. + pub async fn get_items_count_by_key( + &self, + range: impl Into>, + ) -> Result + where + T: Indexed, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize, + { + let range = self.serializer.encode_key_range::(range)?; + let object_store = self.transaction.object_store(T::OBJECT_STORE)?; + let count = if let Some(index) = K::INDEX { + object_store.index(index)?.count_with_key(&range)?.await? + } else { + object_store.count_with_key(&range)?.await? + }; + Ok(count as usize) + } + + /// Query IndexedDB for the number of items that match the given key + /// components range. + pub async fn get_items_count_by_key_components<'b, T, K>( + &self, + range: impl Into>>, + ) -> Result + where + T: Indexed + 'b, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize + 'b, + { + let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); + self.get_items_count_by_key::(range).await + } + + /// Query IndexedDB for the item with the maximum key in the given range. + pub async fn get_max_item_by_key( + &self, + range: impl Into>, + ) -> Result, TransactionError> + where + T: Indexed, + T::IndexedType: DeserializeOwned, + T::Error: AsyncErrorDeps, + K: IndexedKey + Serialize, + { + let range = self.serializer.encode_key_range::(range)?; + let direction = IdbCursorDirection::Prev; + let object_store = self.transaction.object_store(T::OBJECT_STORE)?; + if let Some(index) = K::INDEX { + object_store + .index(index)? + .open_cursor_with_range_and_direction(&range, direction)? + .await? + .map(|cursor| self.serializer.deserialize(cursor.value())) + .transpose() + .map_err(|e| TransactionError::Serialization(Box::new(e))) + } else { + object_store + .open_cursor_with_range_and_direction(&range, direction)? + .await? + .map(|cursor| self.serializer.deserialize(cursor.value())) + .transpose() + .map_err(|e| TransactionError::Serialization(Box::new(e))) + } + } + + /// Adds an item to the corresponding IndexedDB object + /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already + /// exists, it will be rejected. + pub async fn add_item(&self, item: &T) -> Result<(), TransactionError> + where + T: Indexed + Serialize, + T::IndexedType: Serialize, + T::Error: AsyncErrorDeps, + { + self.transaction + .object_store(T::OBJECT_STORE)? + .add_val_owned( + self.serializer + .serialize(item) + .map_err(|e| TransactionError::Serialization(Box::new(e)))?, + )? + .await + .map_err(Into::into) + } + + /// Puts an item in the corresponding IndexedDB object + /// store, i.e., `T::OBJECT_STORE`. If an item with the same key already + /// exists, it will be overwritten. + pub async fn put_item(&self, item: &T) -> Result<(), TransactionError> + where + T: Indexed + Serialize, + T::IndexedType: Serialize, + T::Error: AsyncErrorDeps, + { + self.transaction + .object_store(T::OBJECT_STORE)? + .put_val_owned( + self.serializer + .serialize(item) + .map_err(|e| TransactionError::Serialization(Box::new(e)))?, + )? + .await + .map_err(Into::into) + } + + /// Delete items in given key range from IndexedDB + pub async fn delete_items_by_key( + &self, + range: impl Into>, + ) -> Result<(), TransactionError> + where + T: Indexed, + K: IndexedKey + Serialize, + { + let range = self.serializer.encode_key_range::(range)?; + let object_store = self.transaction.object_store(T::OBJECT_STORE)?; + if let Some(index) = K::INDEX { + let index = object_store.index(index)?; + if let Some(cursor) = index.open_cursor_with_range(&range)?.await? { + while cursor.key().is_some() { + cursor.delete()?.await?; + cursor.continue_cursor()?.await?; + } + } + } else { + object_store.delete_owned(&range)?.await?; + } + Ok(()) + } + + /// Delete items in the given key component range from + /// IndexedDB + pub async fn delete_items_by_key_components<'b, T, K>( + &self, + range: impl Into>>, + ) -> Result<(), TransactionError> + where + T: Indexed + 'b, + K: IndexedKey + Serialize + 'b, + { + let range: IndexedKeyRange = range.into().encoded(self.serializer.inner()); + self.delete_items_by_key::(range).await + } + + /// Delete item that matches the given key components from + /// IndexedDB + pub async fn delete_item_by_key<'b, T, K>( + &self, + key: K::KeyComponents<'b>, + ) -> Result<(), TransactionError> + where + T: Indexed + 'b, + K: IndexedKey + Serialize + 'b, + { + self.delete_items_by_key_components::(key).await + } + + /// Clear all items of type `T` from the associated object store + /// `T::OBJECT_STORE` from IndexedDB + pub async fn clear(&self) -> Result<(), TransactionError> + where + T: Indexed, + { + self.transaction.object_store(T::OBJECT_STORE)?.clear()?.await.map_err(Into::into) + } +}