feat(sdk): add support for persisting the thread subscription catchup tokens

This commit is contained in:
Benjamin Bouvier
2025-09-01 14:53:37 +02:00
parent 18affe3edd
commit 4adbb4aa88
6 changed files with 94 additions and 5 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ pub use room::{
};
pub use store::{
ComposerDraft, ComposerDraftType, QueueWedgeError, StateChanges, StateStore, StateStoreDataKey,
StateStoreDataValue, StoreError,
StateStoreDataValue, StoreError, ThreadSubscriptionCatchupToken,
};
pub use utils::{
MinimalRoomMemberEvent, MinimalStateEvent, OriginalMinimalStateEvent, RedactedMinimalStateEvent,
@@ -46,7 +46,8 @@ use crate::{
MinimalRoomMemberEvent, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
deserialized_responses::{DisplayName, RawAnySyncOrStrippedState},
store::{
QueueWedgeError, StoredThreadSubscription, traits::compare_thread_subscription_bump_stamps,
QueueWedgeError, StoredThreadSubscription,
traits::{ThreadSubscriptionCatchupToken, compare_thread_subscription_bump_stamps},
},
};
@@ -87,6 +88,7 @@ struct MemoryStoreInner {
dependent_send_queue_events: BTreeMap<OwnedRoomId, Vec<DependentQueuedRequest>>,
seen_knock_requests: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, OwnedUserId>>,
thread_subscriptions: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, StoredThreadSubscription>>,
thread_subscriptions_catchup_tokens: Option<Vec<ThreadSubscriptionCatchupToken>>,
}
/// In-memory, non-persistent implementation of the `StateStore`.
@@ -183,6 +185,10 @@ impl StateStore for MemoryStore {
.get(room_id)
.cloned()
.map(StateStoreDataValue::SeenKnockRequests),
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => inner
.thread_subscriptions_catchup_tokens
.clone()
.map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens),
})
}
@@ -246,6 +252,12 @@ impl StateStore for MemoryStore {
.expect("Session data is not a set of seen join request ids"),
);
}
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
inner.thread_subscriptions_catchup_tokens =
Some(value.into_thread_subscriptions_catchup_tokens().expect(
"Session data is not a list of thread subscription catchup tokens",
));
}
}
Ok(())
@@ -276,6 +288,9 @@ impl StateStore for MemoryStore {
StateStoreDataKey::SeenKnockRequests(room_id) => {
inner.seen_knock_requests.remove(room_id);
}
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
inner.thread_subscriptions_catchup_tokens = None;
}
}
Ok(())
}
+2 -1
View File
@@ -93,7 +93,8 @@ pub use self::{
},
traits::{
ComposerDraft, ComposerDraftType, DynStateStore, IntoStateStore, ServerInfo, StateStore,
StateStoreDataKey, StateStoreDataValue, StateStoreExt, WellKnownResponse,
StateStoreDataKey, StateStoreDataValue, StateStoreExt, ThreadSubscriptionCatchupToken,
WellKnownResponse,
},
};
@@ -1151,6 +1151,36 @@ pub enum StateStoreDataValue {
/// A list of knock request ids marked as seen in a room.
SeenKnockRequests(BTreeMap<OwnedEventId, OwnedUserId>),
/// A list of tokens to continue thread subscriptions catchup.
///
/// See documentation of [`ThreadSubscriptionCatchupToken`] for more
/// details.
ThreadSubscriptionsCatchupTokens(Vec<ThreadSubscriptionCatchupToken>),
}
/// Tokens to use when catching up on thread subscriptions.
///
/// These tokens are created when the client receives some thread subscriptions
/// from sync, but the sync indicates that there are more thread subscriptions
/// available on the server. In this case, it's expected that the client will
/// call the MSC4308 companion endpoint to catch up (back-paginate) on previous
/// thread subscriptions.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ThreadSubscriptionCatchupToken {
/// The token to use as the lower bound when fetching new threads
/// subscriptions.
///
/// In sliding sync, this is the `prev_batch` value of a sliding sync
/// response.
pub from: String,
/// The token to use as the upper bound when fetching new threads
/// subscriptions.
///
/// In sliding sync, it must be set to the `pos` value of the sliding sync
/// *request*, which response received a `prev_batch` token.
pub to: Option<String>,
}
/// Current draft of the composer for the room.
@@ -1222,6 +1252,14 @@ impl StateStoreDataValue {
pub fn into_seen_knock_requests(self) -> Option<BTreeMap<OwnedEventId, OwnedUserId>> {
as_variant!(self, Self::SeenKnockRequests)
}
/// Get this value if it is the data for the thread subscriptions catchup
/// tokens.
pub fn into_thread_subscriptions_catchup_tokens(
self,
) -> Option<Vec<ThreadSubscriptionCatchupToken>> {
as_variant!(self, Self::ThreadSubscriptionsCatchupTokens)
}
}
/// A key for key-value data.
@@ -1258,6 +1296,9 @@ pub enum StateStoreDataKey<'a> {
/// A list of knock request ids marked as seen in a room.
SeenKnockRequests(&'a RoomId),
/// A list of thread subscriptions catchup tokens.
ThreadSubscriptionsCatchupTokens,
}
impl StateStoreDataKey<'_> {
@@ -1294,6 +1335,11 @@ impl StateStoreDataKey<'_> {
/// Key prefix to use for the
/// [`SeenKnockRequests`][Self::SeenKnockRequests] variant.
pub const SEEN_KNOCK_REQUESTS: &'static str = "seen_knock_requests";
/// Key prefix to use for the
/// [`ThreadSubscriptionsCatchupTokens`][Self::ThreadSubscriptionsCatchupTokens] variant.
pub const THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS: &'static str =
"thread_subscriptions_catchup_tokens";
}
/// Compare two thread subscription changes bump stamps, given a fixed room and
@@ -32,7 +32,7 @@ use matrix_sdk_base::{
StateStore, StoreError, StoredThreadSubscription, ThreadSubscriptionStatus,
},
MinimalRoomMemberEvent, RoomInfo, RoomMemberships, StateStoreDataKey, StateStoreDataValue,
ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK,
ThreadSubscriptionCatchupToken, ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK,
};
use matrix_sdk_store_encryption::{Error as EncryptionError, StoreCipher};
use ruma::{
@@ -439,6 +439,9 @@ impl IndexeddbStateStore {
StateStoreDataKey::SeenKnockRequests(room_id) => {
self.encode_key(keys::KV, (StateStoreDataKey::SEEN_KNOCK_REQUESTS, room_id))
}
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
self.encode_key(keys::KV, StateStoreDataKey::THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS)
}
}
}
}
@@ -591,6 +594,10 @@ impl_state_store!({
.map(|f| self.deserialize_value::<BTreeMap<OwnedEventId, OwnedUserId>>(&f))
.transpose()?
.map(StateStoreDataValue::SeenKnockRequests),
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => value
.map(|f| self.deserialize_value::<Vec<ThreadSubscriptionCatchupToken>>(&f))
.transpose()?
.map(StateStoreDataValue::ThreadSubscriptionsCatchupTokens),
};
Ok(value)
@@ -632,6 +639,11 @@ impl_state_store!({
.into_seen_knock_requests()
.expect("Session data is not a set of seen knock request ids"),
),
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => self.serialize_value(
&value
.into_thread_subscriptions_catchup_tokens()
.expect("Session data is not a list of thread subscription catchup tokens"),
),
};
let tx =
+16 -1
View File
@@ -40,7 +40,7 @@ use ruma::{
use rusqlite::{OptionalExtension, Transaction};
use serde::{Deserialize, Serialize};
use tokio::fs;
use tracing::{debug, warn};
use tracing::{debug, trace, warn};
use crate::{
error::{Error, Result},
@@ -416,6 +416,9 @@ impl SqliteStateStore {
StateStoreDataKey::SeenKnockRequests(room_id) => {
Cow::Owned(format!("{}:{room_id}", StateStoreDataKey::SEEN_KNOCK_REQUESTS))
}
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
Cow::Borrowed(StateStoreDataKey::THREAD_SUBSCRIPTIONS_CATCHUP_TOKENS)
}
};
self.encode_key(keys::KV_BLOB, &*key_s)
@@ -1037,6 +1040,11 @@ impl StateStore for SqliteStateStore {
StateStoreDataKey::SeenKnockRequests(_) => {
StateStoreDataValue::SeenKnockRequests(self.deserialize_value(&data)?)
}
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => {
StateStoreDataValue::ThreadSubscriptionsCatchupTokens(
self.deserialize_value(&data)?,
)
}
})
})
.transpose()
@@ -1077,6 +1085,11 @@ impl StateStore for SqliteStateStore {
.into_seen_knock_requests()
.expect("Session data is not a set of seen knock request ids"),
)?,
StateStoreDataKey::ThreadSubscriptionsCatchupTokens => self.serialize_value(
&value
.into_thread_subscriptions_catchup_tokens()
.expect("Session data is not a list of thread subscription catchup tokens"),
)?,
};
self.acquire()
@@ -2125,9 +2138,11 @@ impl StateStore for SqliteStateStore {
if let Some(previous) = self.load_thread_subscription(room_id, thread_id).await? {
if previous == new {
// No need to update anything.
trace!("not saving thread subscription because the subscription is the same");
return Ok(());
}
if !compare_thread_subscription_bump_stamps(previous.bump_stamp, &mut new.bump_stamp) {
trace!("not saving thread subscription because we have a newer bump stamp");
return Ok(());
}
}