Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions crates/matrix-sdk-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ js = [
"matrix-sdk-store-encryption/js",
]
qrcode = ["matrix-sdk-crypto?/qrcode"]
automatic-room-key-forwarding = ["matrix-sdk-crypto?/automatic-room-key-forwarding"]
experimental-send-custom-to-device = ["matrix-sdk-crypto?/experimental-send-custom-to-device"]
automatic-room-key-forwarding = [
"matrix-sdk-crypto?/automatic-room-key-forwarding",
]
experimental-send-custom-to-device = [
"matrix-sdk-crypto?/experimental-send-custom-to-device",
]
uniffi = ["dep:uniffi", "matrix-sdk-crypto?/uniffi", "matrix-sdk-common/uniffi"]

# Private feature, see
Expand Down Expand Up @@ -101,6 +105,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }

[target.'cfg(target_family = "wasm")'.dev-dependencies]
wasm-bindgen-test.workspace = true
gloo-timers = { workspace = true, features = ["futures"] }

[lints]
workspace = true
Original file line number Diff line number Diff line change
Expand Up @@ -1432,11 +1432,14 @@ macro_rules! event_cache_store_integration_tests {
#[macro_export]
macro_rules! event_cache_store_integration_tests_time {
() => {
#[cfg(not(target_family = "wasm"))]
mod event_cache_store_integration_tests_time {
use std::time::Duration;

#[cfg(all(target_family = "wasm", target_os = "unknown"))]
use gloo_timers::future::sleep;
use matrix_sdk_test::async_test;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use tokio::time::sleep;
use $crate::event_cache::store::IntoEventCacheStore;

use super::get_event_cache_store;
Expand Down Expand Up @@ -1465,26 +1468,26 @@ macro_rules! event_cache_store_integration_tests_time {
assert!(!acquired5);

// That's a nice test we got here, go take a little nap.
tokio::time::sleep(Duration::from_millis(50)).await;
sleep(Duration::from_millis(50)).await;

// Still too early.
let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
assert!(!acquired55);

// Ok you can take another nap then.
tokio::time::sleep(Duration::from_millis(250)).await;
sleep(Duration::from_millis(250)).await;

// At some point, we do get the lock.
let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap();
assert!(acquired6);

tokio::time::sleep(Duration::from_millis(1)).await;
sleep(Duration::from_millis(1)).await;

// The other gets it almost immediately too.
let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap();
assert!(acquired7);

tokio::time::sleep(Duration::from_millis(1)).await;
sleep(Duration::from_millis(1)).await;

// But when we take a longer lease...
let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap();
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-indexeddb/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ getrandom = { workspace = true, features = ["js"] }
[dev-dependencies]
assert_matches.workspace = true
assert_matches2.workspace = true
gloo-timers = { workspace = true, features = ["futures"] }
matrix-sdk-base = { workspace = true, features = ["testing"] }
matrix-sdk-common = { workspace = true, features = ["js"] }
matrix-sdk-crypto = { workspace = true, features = ["js", "testing"] }
Expand Down
14 changes: 12 additions & 2 deletions crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ pub mod v1 {

pub mod keys {
pub const CORE: &str = "core";
pub const LEASES: &str = "leases";
pub const LEASES_KEY_PATH: &str = "id";
pub const ROOMS: &str = "rooms";
pub const LINKED_CHUNKS: &str = "linked_chunks";
pub const LINKED_CHUNKS_KEY_PATH: &str = "id";
Expand All @@ -129,19 +131,27 @@ 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)?;
create_gaps_object_store(db)?;
Ok(())
}

/// Create an object store for tracking miscellaneous information, e.g.,
/// leases locks
/// Create an object store for tracking miscellaneous information
fn create_core_object_store(db: &IdbDatabase) -> Result<(), DomException> {
let _ = db.create_object_store(keys::CORE)?;
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();
object_store_params.key_path(Some(&keys::LEASES_KEY_PATH.into()));
let _ = db.create_object_store_with_params(keys::LEASES, &object_store_params)?;
Ok(())
}

/// Create an object store for tracking information about linked chunks.
///
/// * Primary Key - `id`
Expand Down
47 changes: 39 additions & 8 deletions crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#![allow(unused)]

use std::time::Duration;

use indexed_db_futures::IdbDatabase;
use matrix_sdk_base::{
event_cache::{
Expand All @@ -30,15 +32,18 @@ use matrix_sdk_base::{
media::MediaRequestParameters,
timer,
};
use ruma::{events::relation::RelationType, EventId, MxcUri, OwnedEventId, RoomId};
use ruma::{
events::relation::RelationType, EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId,
RoomId,
};
use tracing::{error, instrument, trace};
use web_sys::IdbTransactionMode;

use crate::event_cache_store::{
migrations::current::keys,
serializer::IndexeddbEventCacheStoreSerializer,
serializer::{traits::Indexed, IndexeddbEventCacheStoreSerializer},
transaction::{IndexeddbEventCacheStoreTransaction, IndexeddbEventCacheStoreTransactionError},
types::{ChunkType, InBandEvent, OutOfBandEvent},
types::{ChunkType, InBandEvent, Lease, OutOfBandEvent},
};

mod builder;
Expand Down Expand Up @@ -132,10 +137,27 @@ impl_event_cache_store! {
holder: &str,
) -> Result<bool, IndexeddbEventCacheStoreError> {
let _timer = timer!("method");
self.memory_store
.try_take_leased_lock(lease_duration_ms, key, holder)
.await
.map_err(IndexeddbEventCacheStoreError::MemoryStore)

let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into());

let transaction =
self.transaction(&[Lease::OBJECT_STORE], IdbTransactionMode::Readwrite)?;

if let Some(lease) = transaction.get_lease_by_id(key).await? {
if lease.holder != holder && !lease.has_expired(now) {
return Ok(false);
}
}

transaction
.put_lease(&Lease {
key: key.to_owned(),
holder: holder.to_owned(),
expiration: now + Duration::from_millis(lease_duration_ms.into()),
})
.await?;

Ok(true)
}

#[instrument(skip(self, updates))]
Expand Down Expand Up @@ -649,7 +671,10 @@ impl_event_cache_store! {

#[cfg(test)]
mod tests {
use matrix_sdk_base::event_cache::store::{EventCacheStore, EventCacheStoreError};
use matrix_sdk_base::{
event_cache::store::{EventCacheStore, EventCacheStoreError},
event_cache_store_integration_tests_time,
};
use matrix_sdk_test::async_test;
use uuid::Uuid;

Expand All @@ -673,6 +698,9 @@ mod tests {

#[cfg(target_family = "wasm")]
indexeddb_event_cache_store_integration_tests!();

#[cfg(target_family = "wasm")]
event_cache_store_integration_tests_time!();
}

mod encrypted {
Expand All @@ -690,5 +718,8 @@ mod tests {

#[cfg(target_family = "wasm")]
indexeddb_event_cache_store_integration_tests!();

#[cfg(target_family = "wasm")]
event_cache_store_integration_tests_time!();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use crate::{
Indexed, IndexedKey, IndexedKeyBounds, IndexedKeyComponentBounds,
IndexedPrefixKeyBounds, IndexedPrefixKeyComponentBounds,
},
types::{Chunk, Event, Gap, Position},
types::{Chunk, Event, Gap, Lease, Position},
},
serializer::{IndexeddbSerializer, MaybeEncrypted},
};
Expand All @@ -65,6 +65,14 @@ const INDEXED_KEY_LOWER_CHARACTER: char = '\u{0000}';
/// [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<String> =
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<String> =
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
Expand Down Expand Up @@ -223,6 +231,70 @@ impl<K> From<K> for IndexedKeyRange<K> {
}
}

/// Represents the [`LEASES`][1] object store.
///
/// [1]: crate::event_cache_store::migrations::v1::create_lease_object_store
#[derive(Debug, Serialize, Deserialize)]
pub struct IndexedLease {
/// The primary key of the object store.
pub id: IndexedLeaseIdKey,
/// The (possibly encrypted) content - i.e., a [`Lease`].
pub content: IndexedLeaseContent,
}

impl Indexed for Lease {
type IndexedType = IndexedLease;

const OBJECT_STORE: &'static str = keys::LEASES;

type Error = CryptoStoreError;

fn to_indexed(
&self,
serializer: &IndexeddbSerializer,
) -> Result<Self::IndexedType, Self::Error> {
Ok(IndexedLease {
id: IndexedLeaseIdKey::encode(&self.key, serializer),
content: serializer.maybe_encrypt_value(self)?,
})
}

fn from_indexed(
indexed: Self::IndexedType,
serializer: &IndexeddbSerializer,
) -> Result<Self, Self::Error> {
serializer.maybe_decrypt_value(indexed.content)
}
}

/// The value associated with the [primary key](IndexedLease::id) of the
/// [`LEASES`][1] object store, which is constructed from the value in
/// [`Lease::key`]. This value may or may not be hashed depending on the
/// provided [`IndexeddbSerializer`].
///
/// [1]: crate::event_cache_store::migrations::v1::create_linked_chunks_object_store
pub type IndexedLeaseIdKey = String;

impl IndexedKey<Lease> for IndexedLeaseIdKey {
type KeyComponents<'a> = &'a str;

fn encode(components: Self::KeyComponents<'_>, serializer: &IndexeddbSerializer) -> Self {
serializer.encode_key_as_string(keys::LEASES, components)
}
}

impl IndexedKeyComponentBounds<Lease> for IndexedLeaseIdKey {
fn lower_key_components() -> Self::KeyComponents<'static> {
INDEXED_KEY_LOWER_STRING.as_str()
}

fn upper_key_components() -> Self::KeyComponents<'static> {
INDEXED_KEY_UPPER_STRING.as_str()
}
}

pub type IndexedLeaseContent = MaybeEncrypted;

/// Represents the [`LINKED_CHUNKS`][1] object store.
///
/// [1]: crate::event_cache_store::migrations::v1::create_linked_chunks_object_store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ use crate::event_cache_store::{
},
types::{
IndexedChunkIdKey, IndexedEventIdKey, IndexedEventPositionKey, IndexedEventRelationKey,
IndexedGapIdKey, IndexedKeyRange, IndexedNextChunkIdKey,
IndexedGapIdKey, IndexedKeyRange, IndexedLeaseIdKey, IndexedNextChunkIdKey,
},
IndexeddbEventCacheStoreSerializer,
},
types::{Chunk, ChunkType, Event, Gap, Position},
types::{Chunk, ChunkType, Event, Gap, Lease, Position},
};

#[derive(Debug, Error)]
Expand Down Expand Up @@ -410,6 +410,24 @@ impl<'a> IndexeddbEventCacheStoreTransaction<'a> {
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<Option<Lease>, IndexeddbEventCacheStoreTransactionError> {
self.get_item_by_key_components::<Lease, IndexedLeaseIdKey>(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> {
self.put_item(lease).await
}

/// Query IndexedDB for chunks that match the given chunk identifier in the
/// given room. If more than one item is found, an error is returned.
pub async fn get_chunk_by_id(
Expand Down
18 changes: 18 additions & 0 deletions crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,31 @@
// See the License for the specific language governing permissions and
// limitations under the License

use std::time::Duration;

use matrix_sdk_base::{
deserialized_responses::TimelineEvent, event_cache::store::extract_event_relation,
linked_chunk::ChunkIdentifier,
};
use ruma::{OwnedEventId, OwnedRoomId, RoomId};
use serde::{Deserialize, Serialize};

/// Representation of a time-based lock on the entire
/// [`IndexeddbEventCacheStore`](crate::event_cache_store::IndexeddbEventCacheStore)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Lease {
pub key: String,
pub holder: String,
pub expiration: Duration,
}

impl Lease {
/// Determines whether the lease is expired at a given time `t`
pub fn has_expired(&self, t: Duration) -> bool {
self.expiration < t
}
}

/// Representation of a [`Chunk`](matrix_sdk_base::linked_chunk::Chunk)
/// which can be stored in IndexedDB.
#[derive(Debug, Serialize, Deserialize)]
Expand Down
Loading