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 f5d06fcfe9a..4f478289a6e 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -20,20 +20,26 @@ use thiserror::Error; use wasm_bindgen::JsValue; use web_sys::{DomException, IdbIndexParameters}; -const CURRENT_DB_VERSION: Version = Version::V1; +/// The current version and keys used in the database. +pub mod current { + use super::{v1, Version}; + + pub const VERSION: Version = Version::V1; + pub use v1::keys; +} /// Opens a connection to the IndexedDB database and takes care of upgrading it /// if necessary. #[allow(unused)] pub async fn open_and_upgrade_db(name: &str) -> Result { - let mut request = IdbDatabase::open_u32(name, CURRENT_DB_VERSION as u32)?; + let mut request = IdbDatabase::open_u32(name, current::VERSION as u32)?; request.set_on_upgrade_needed(Some(|event: &IdbVersionChangeEvent| -> Result<(), JsValue> { let mut version = Version::try_from(event.old_version() as u32).map_err(DomException::from)?; - while version < CURRENT_DB_VERSION { + while version < current::VERSION { version = match version.upgrade(event.db())? { Some(next) => next, - None => CURRENT_DB_VERSION, /* No more upgrades to apply, jump forward! */ + None => current::VERSION, /* No more upgrades to apply, jump forward! */ }; } Ok(()) @@ -103,6 +109,7 @@ pub mod v1 { pub mod keys { pub const CORE: &str = "core"; + pub const ROOMS: &str = "rooms"; pub const LINKED_CHUNKS: &str = "linked_chunks"; pub const LINKED_CHUNKS_KEY_PATH: &str = "id"; pub const LINKED_CHUNKS_NEXT: &str = "linked_chunks_next"; @@ -113,6 +120,8 @@ pub mod v1 { pub const EVENTS_POSITION_KEY_PATH: &str = "position"; pub const EVENTS_RELATION: &str = "events_relation"; pub const EVENTS_RELATION_KEY_PATH: &str = "relation"; + pub const EVENTS_RELATION_RELATED_EVENTS: &str = "events_relation_related_event"; + pub const EVENTS_RELATION_RELATION_TYPES: &str = "events_relation_relation_type"; pub const GAPS: &str = "gaps"; pub const GAPS_KEY_PATH: &str = "id"; } 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 11a97c5a762..9211bdb0cc7 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License +#![allow(unused)] + mod migrations; mod serializer; mod types; 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 8766e82b084..d17fe6663ae 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,4 +12,146 @@ // See the License for the specific language governing permissions and // limitations under the License +use gloo_utils::format::JsValueSerdeExt; +use matrix_sdk_crypto::CryptoStoreError; +use ruma::RoomId; +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, IndexedKeyBounds}, + serializer::IndexeddbSerializer, +}; + +mod traits; mod types; + +#[derive(Debug, Error)] +pub enum IndexeddbEventCacheStoreSerializerError { + #[error("indexing: {0}")] + Indexing(Box), + #[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 +pub struct IndexeddbEventCacheStoreSerializer { + inner: IndexeddbSerializer, +} + +impl IndexeddbEventCacheStoreSerializer { + pub fn new(inner: IndexeddbSerializer) -> Self { + 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, room_id: &RoomId, components: &K::KeyComponents) -> K + where + T: Indexed, + K: IndexedKey, + { + K::encode(room_id, 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, + room_id: &RoomId, + components: &K::KeyComponents, + ) -> Result + where + T: Indexed, + K: IndexedKey + Serialize, + { + serde_wasm_bindgen::to_value(&self.encode_key::(room_id, components)) + } + + /// Encodes the entire key 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, + room_id: &RoomId, + ) -> Result + where + T: Indexed, + K: IndexedKeyBounds + Serialize, + { + let lower = serde_wasm_bindgen::to_value(&K::encode_lower(room_id, &self.inner))?; + let upper = serde_wasm_bindgen::to_value(&K::encode_upper(room_id, &self.inner))?; + IdbKeyRange::bound(&lower, &upper).map_err(Into::into) + } + + /// Encodes a bounded key range for an [`Indexed`] type from `lower` to + /// `upper`. + /// + /// Note that the particular key which is encoded is defined by the type + /// `K`. + pub fn encode_key_range_from_to( + &self, + room_id: &RoomId, + lower: &K::KeyComponents, + upper: &K::KeyComponents, + ) -> Result + where + T: Indexed, + K: IndexedKeyBounds + Serialize, + { + let lower = serde_wasm_bindgen::to_value(&K::encode(room_id, lower, &self.inner))?; + let upper = serde_wasm_bindgen::to_value(&K::encode(room_id, upper, &self.inner))?; + IdbKeyRange::bound(&lower, &upper).map_err(Into::into) + } + + /// Serializes an [`Indexed`] type into a [`JsValue`] + pub fn serialize( + &self, + room_id: &RoomId, + t: &T, + ) -> Result + where + T: Indexed, + T::IndexedType: Serialize, + T::Error: std::error::Error + 'static, + { + let indexed = t + .to_indexed(room_id, &self.inner) + .map_err(|e| IndexeddbEventCacheStoreSerializerError::Indexing(Box::new(e)))?; + 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, + T::Error: std::error::Error + 'static, + { + let indexed: T::IndexedType = value.into_serde()?; + T::from_indexed(indexed, &self.inner) + .map_err(|e| IndexeddbEventCacheStoreSerializerError::Indexing(Box::new(e))) + } +} diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs new file mode 100644 index 00000000000..cd4ad58165c --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/traits.rs @@ -0,0 +1,76 @@ +// 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 ruma::RoomId; + +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 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, + room_id: &RoomId, + 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 { + /// Any extra data used to construct the key. + type KeyComponents; + + /// 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( + room_id: &RoomId, + components: &Self::KeyComponents, + serializer: &IndexeddbSerializer, + ) -> Self; +} + +/// A trait for constructing the bounds of an [`IndexedKey`]. +/// +/// This is useful when constructing range queries in IndexedDB. +pub trait IndexedKeyBounds: IndexedKey { + /// Encodes the lower bound of the key. + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self; + + /// Encodes the upper bound of the key. + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self; +} diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs index 93f3e4199af..2669c4ec374 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/serializer/types.rs @@ -27,9 +27,38 @@ //! These types mimic the structure of the object stores and indices created in //! [`crate::event_cache_store::migrations`]. +use matrix_sdk_base::linked_chunk::ChunkIdentifier; +use matrix_sdk_crypto::CryptoStoreError; +use ruma::{events::relation::RelationType, EventId, OwnedEventId, RoomId}; use serde::{Deserialize, Serialize}; +use thiserror::Error; -use crate::serializer::MaybeEncrypted; +use crate::{ + event_cache_store::{ + migrations::current::keys, + serializer::traits::{Indexed, IndexedKey, IndexedKeyBounds}, + types::{Chunk, Event, Gap, Position}, + }, + serializer::{IndexeddbSerializer, MaybeEncrypted}, +}; + +/// 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}'; /// Represents the [`LINKED_CHUNKS`][1] object store. /// @@ -46,6 +75,38 @@ pub struct IndexedChunk { pub content: IndexedChunkContent, } +impl Indexed for Chunk { + type IndexedType = IndexedChunk; + type Error = CryptoStoreError; + + fn to_indexed( + &self, + room_id: &RoomId, + serializer: &IndexeddbSerializer, + ) -> Result { + Ok(IndexedChunk { + id: >::encode( + room_id, + &ChunkIdentifier::new(self.identifier), + serializer, + ), + next: IndexedNextChunkIdKey::encode( + room_id, + &self.next.map(ChunkIdentifier::new), + serializer, + ), + content: serializer.maybe_encrypt_value(self)?, + }) + } + + fn from_indexed( + indexed: Self::IndexedType, + serializer: &IndexeddbSerializer, + ) -> Result { + serializer.maybe_decrypt_value(indexed.content) + } +} + /// The value associated with the [primary key](IndexedChunk::id) of the /// [`LINKED_CHUNKS`][1] object store, which is constructed from: /// @@ -56,6 +117,34 @@ pub struct IndexedChunk { #[derive(Debug, Serialize, Deserialize)] pub struct IndexedChunkIdKey(IndexedRoomId, IndexedChunkId); +impl IndexedKey for IndexedChunkIdKey { + type KeyComponents = ChunkIdentifier; + + fn encode( + room_id: &RoomId, + chunk_id: &ChunkIdentifier, + serializer: &IndexeddbSerializer, + ) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + let chunk_id = chunk_id.index(); + Self(room_id, chunk_id) + } +} + +impl IndexedKeyBounds for IndexedChunkIdKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode(room_id, &ChunkIdentifier::new(0), serializer) + } + + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode( + room_id, + &ChunkIdentifier::new(js_sys::Number::MAX_SAFE_INTEGER as u64), + serializer, + ) + } +} + pub type IndexedRoomId = String; pub type IndexedChunkId = u64; pub type IndexedChunkContent = MaybeEncrypted; @@ -84,6 +173,47 @@ pub enum IndexedNextChunkIdKey { Some(IndexedChunkIdKey), } +impl IndexedNextChunkIdKey { + pub fn none(room_id: IndexedRoomId) -> Self { + Self::None((room_id,)) + } +} + +impl IndexedKey for IndexedNextChunkIdKey { + type KeyComponents = Option; + + fn encode( + room_id: &RoomId, + next_chunk_id: &Option, + serializer: &IndexeddbSerializer, + ) -> Self { + next_chunk_id + .map(|id| { + Self::Some(>::encode( + room_id, &id, serializer, + )) + }) + .unwrap_or_else(|| { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + Self::none(room_id) + }) + } +} + +impl IndexedKeyBounds for IndexedNextChunkIdKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode(room_id, &None, serializer) + } + + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode( + room_id, + &Some(ChunkIdentifier::new(js_sys::Number::MAX_SAFE_INTEGER as u64)), + serializer, + ) + } +} + /// Represents the [`EVENTS`][1] object store. /// /// [1]: crate::event_cache_store::migrations::v1::create_events_object_store @@ -101,6 +231,46 @@ pub struct IndexedEvent { pub content: IndexedEventContent, } +#[derive(Debug, Error)] +pub enum IndexedEventError { + #[error("no event id")] + NoEventId, + #[error("crypto store: {0}")] + CryptoStore(#[from] CryptoStoreError), +} + +impl Indexed for Event { + type IndexedType = IndexedEvent; + type Error = IndexedEventError; + + fn to_indexed( + &self, + room_id: &RoomId, + serializer: &IndexeddbSerializer, + ) -> Result { + let event_id = self.event_id().ok_or(Self::Error::NoEventId)?; + let id = IndexedEventIdKey::encode(room_id, &event_id, serializer); + let position = self + .position() + .map(|position| IndexedEventPositionKey::encode(room_id, &position, serializer)); + let relation = self.relation().map(|(related_event, relation_type)| { + IndexedEventRelationKey::encode( + room_id, + &(related_event, RelationType::from(relation_type)), + serializer, + ) + }); + Ok(IndexedEvent { id, position, relation, content: serializer.maybe_encrypt_value(self)? }) + } + + fn from_indexed( + indexed: Self::IndexedType, + serializer: &IndexeddbSerializer, + ) -> Result { + serializer.maybe_decrypt_value(indexed.content).map_err(Into::into) + } +} + /// The value associated with the [primary key](IndexedEvent::id) of the /// [`EVENTS`][1] object store, which is constructed from: /// @@ -111,6 +281,30 @@ pub struct IndexedEvent { #[derive(Debug, Serialize, Deserialize)] pub struct IndexedEventIdKey(IndexedRoomId, IndexedEventId); +impl IndexedKey for IndexedEventIdKey { + type KeyComponents = OwnedEventId; + + fn encode(room_id: &RoomId, event_id: &OwnedEventId, serializer: &IndexeddbSerializer) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + let event_id = serializer.encode_key_as_string(keys::EVENTS, event_id); + Self(room_id, event_id) + } +} + +impl IndexedKeyBounds for IndexedEventIdKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + let event_id = String::from(INDEXED_KEY_LOWER_CHARACTER); + Self(room_id, event_id) + } + + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + let event_id = String::from(INDEXED_KEY_UPPER_CHARACTER); + Self(room_id, event_id) + } +} + pub type IndexedEventId = String; /// The value associated with the [`position`](IndexedEvent::position) index of @@ -124,6 +318,36 @@ pub type IndexedEventId = String; #[derive(Debug, Serialize, Deserialize)] pub struct IndexedEventPositionKey(IndexedRoomId, IndexedChunkId, IndexedEventPositionIndex); +impl IndexedKey for IndexedEventPositionKey { + type KeyComponents = Position; + + fn encode(room_id: &RoomId, position: &Position, serializer: &IndexeddbSerializer) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + Self(room_id, position.chunk_identifier, position.index) + } +} + +impl IndexedKeyBounds for IndexedEventPositionKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode( + room_id, + &Position { chunk_identifier: 0, index: 0 }, + serializer, + ) + } + + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode( + room_id, + &Position { + chunk_identifier: js_sys::Number::MAX_SAFE_INTEGER as u64, + index: js_sys::Number::MAX_SAFE_INTEGER as usize, + }, + serializer, + ) + } +} + pub type IndexedEventPositionIndex = usize; /// The value associated with the [`relation`](IndexedEvent::relation) index of @@ -137,6 +361,39 @@ pub type IndexedEventPositionIndex = usize; #[derive(Debug, Serialize, Deserialize)] pub struct IndexedEventRelationKey(IndexedRoomId, IndexedEventId, IndexedRelationType); +impl IndexedKey for IndexedEventRelationKey { + type KeyComponents = (OwnedEventId, RelationType); + + fn encode( + room_id: &RoomId, + (related_event_id, relation_type): &(OwnedEventId, RelationType), + serializer: &IndexeddbSerializer, + ) -> Self { + let room_id = serializer.encode_key_as_string(keys::ROOMS, room_id); + let related_event_id = + serializer.encode_key_as_string(keys::EVENTS_RELATION_RELATED_EVENTS, related_event_id); + let relation_type = serializer + .encode_key_as_string(keys::EVENTS_RELATION_RELATION_TYPES, relation_type.to_string()); + Self(room_id, related_event_id, relation_type) + } +} + +impl IndexedKeyBounds for IndexedEventRelationKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> 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 encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> 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); + Self(room_id, related_event_id, relation_type) + } +} + /// A representation of the relationship between two events (see /// [`RelationType`](ruma::events::relation::RelationType)) pub type IndexedRelationType = String; @@ -154,6 +411,33 @@ pub struct IndexedGap { pub content: IndexedGapContent, } +impl Indexed for Gap { + type IndexedType = IndexedGap; + type Error = CryptoStoreError; + + fn to_indexed( + &self, + room_id: &RoomId, + serializer: &IndexeddbSerializer, + ) -> Result { + Ok(IndexedGap { + id: >::encode( + room_id, + &ChunkIdentifier::new(self.chunk_identifier), + serializer, + ), + content: serializer.maybe_encrypt_value(self)?, + }) + } + + fn from_indexed( + indexed: Self::IndexedType, + serializer: &IndexeddbSerializer, + ) -> Result { + serializer.maybe_decrypt_value(indexed.content) + } +} + /// The primary key of the [`GAPS`][1] object store, which is constructed from: /// /// - The (possibly) encrypted Room ID @@ -162,4 +446,26 @@ pub struct IndexedGap { /// [1]: crate::event_cache_store::migrations::v1::create_gaps_object_store pub type IndexedGapIdKey = IndexedChunkIdKey; +impl IndexedKey for IndexedGapIdKey { + type KeyComponents = >::KeyComponents; + + fn encode( + room_id: &RoomId, + components: &Self::KeyComponents, + serializer: &IndexeddbSerializer, + ) -> Self { + >::encode(room_id, components, serializer) + } +} + +impl IndexedKeyBounds for IndexedGapIdKey { + fn encode_lower(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode_lower(room_id, serializer) + } + + fn encode_upper(room_id: &RoomId, serializer: &IndexeddbSerializer) -> Self { + >::encode_upper(room_id, serializer) + } +} + pub type IndexedGapContent = MaybeEncrypted; diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs index 7cea26b041b..d2f90f76cea 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs @@ -1,4 +1,22 @@ -use matrix_sdk_base::{deserialized_responses::TimelineEvent, linked_chunk::ChunkIdentifier}; +// 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::{ + deserialized_responses::TimelineEvent, event_cache::store::extract_event_relation, + linked_chunk::ChunkIdentifier, +}; +use ruma::OwnedEventId; use serde::{Deserialize, Serialize}; /// Representation of a [`Chunk`](matrix_sdk_base::linked_chunk::Chunk) @@ -50,6 +68,34 @@ impl From for TimelineEvent { } } +impl Event { + /// The [`OwnedEventId`] of the underlying event. + pub fn event_id(&self) -> Option { + match self { + Event::InBand(e) => e.event_id(), + Event::OutOfBand(e) => e.event_id(), + } + } + + /// The [`Position`] of the underlying event, if it is in a chunk. + pub fn position(&self) -> Option { + match self { + Event::InBand(e) => Some(e.position), + Event::OutOfBand(_) => None, + } + } + + /// The [`OwnedEventId`] and + /// [`RelationType`](ruma::events::relation::RelationType) of the underlying + /// event as a [`String`]. + pub fn relation(&self) -> Option<(OwnedEventId, String)> { + match self { + Event::InBand(e) => e.relation(), + Event::OutOfBand(e) => e.relation(), + } + } +} + /// A generic representation of an /// [`Event`](matrix_sdk_base::event_cache::Event) which can be stored in /// IndexedDB. @@ -64,6 +110,21 @@ pub struct GenericEvent

{ pub position: P, } +impl

GenericEvent

{ + /// The [`OwnedEventId`] of the underlying event. + pub fn event_id(&self) -> Option { + self.content.event_id() + } + + /// The event that the underlying event relates to, if any. + /// + /// Returns the related [`OwnedEventId`] and the + /// [`RelationType`](ruma::events::relation::RelationType) as a [`String`]. + pub fn relation(&self) -> Option<(OwnedEventId, String)> { + extract_event_relation(self.content.raw()) + } +} + /// A concrete instance of [`GenericEvent`] for in-band events, i.e., /// events which are part of a chunk and therefore have a position. pub type InBandEvent = GenericEvent; @@ -98,6 +159,8 @@ impl From for Position { /// which can be stored in IndexedDB. #[derive(Debug, Serialize, Deserialize)] pub struct Gap { + /// The identifier of the chunk containing this gap. + pub chunk_identifier: u64, /// The token to use in the query, extracted from a previous "from" / /// "end" field of a `/messages` response. pub prev_token: String,