matrix_sdk/sliding_sync/
sticky_parameters.rs

1//! Sticky parameters are a way to spare bandwidth on the network, by sending
2//! request parameters once and have the server remember them.
3//!
4//! The set of sticky parameters have to be agreed upon by the server and the
5//! client; this is defined in the
6//! [MSC](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/sync-v3/proposals/3575-sync.md).
7
8use ruma::{OwnedTransactionId, TransactionId};
9
10/// An `OwnedTransactionId` that is either initialized at creation, or
11/// lazily-generated once.
12#[derive(Debug)]
13pub struct LazyTransactionId {
14    txn_id: Option<OwnedTransactionId>,
15}
16
17impl LazyTransactionId {
18    /// Create a new `LazyTransactionId`, not set.
19    pub fn new() -> Self {
20        Self { txn_id: None }
21    }
22
23    /// Get (or create it, if never set) a `TransactionId`.
24    pub fn get_or_create(&mut self) -> &TransactionId {
25        self.txn_id.get_or_insert_with(TransactionId::new)
26    }
27
28    /// Attempt to get the underlying `TransactionId` without creating it, if
29    /// missing.
30    pub fn get(&self) -> Option<&TransactionId> {
31        self.txn_id.as_deref()
32    }
33}
34
35#[cfg(test)]
36impl LazyTransactionId {
37    /// Create a `LazyTransactionId` for a given known transaction id. For
38    /// testing only.
39    pub fn from_owned(owned: OwnedTransactionId) -> Self {
40        Self { txn_id: Some(owned) }
41    }
42}
43
44/// A trait to implement for data that can be sticky, given a context.
45pub trait StickyData {
46    /// Request type that will be applied to, if the sticky parameters have been
47    /// invalidated before.
48    type Request;
49
50    /// Apply the current data onto the request.
51    fn apply(&self, request: &mut Self::Request);
52
53    /// When the current are committed, i.e. when the request has been validated
54    /// by a response.
55    fn on_commit(&mut self) {
56        // noop
57    }
58}
59
60/// Helper data structure to manage sticky parameters, for any kind of data.
61///
62/// Initially, the provided data is considered to be invalidated, so it's
63/// applied onto the request the first time it's sent. Any changes to the
64/// wrapped data happen via [`Self::data_mut`], which invalidates the sticky
65/// parameters; they will be applied automatically to the next request.
66///
67/// When applying sticky parameters, we will also remember the transaction id
68/// that was generated for us, stash it, so we can match the response against
69/// the transaction id later, and only consider the data isn't invalidated
70/// anymore (we say it's "committed" in that case) if the response's transaction
71/// id match what we expect.
72#[derive(Debug)]
73pub struct SlidingSyncStickyManager<D: StickyData> {
74    /// The data managed by this sticky manager.
75    data: D,
76
77    /// Was any of the parameters invalidated? If yes, reinitialize them.
78    invalidated: bool,
79
80    /// If the sticky parameters were applied to a given request, this is
81    /// the transaction id generated for that request, that must be matched
82    /// upon in the next call to `commit()`.
83    txn_id: Option<OwnedTransactionId>,
84}
85
86impl<D: StickyData> SlidingSyncStickyManager<D> {
87    /// Create a new `StickyManager` for the given data.
88    ///
89    /// Always assume the initial data invalidates the request, at first.
90    pub fn new(data: D) -> Self {
91        Self { data, txn_id: None, invalidated: true }
92    }
93
94    /// Get a mutable reference to the managed data.
95    ///
96    /// Will invalidate the sticky set by default. If you don't need to modify
97    /// the data, use `Self::data()`; if you're not sure you're going to modify
98    /// the data, it's best to first use `Self::data()` then `Self::data_mut()`
99    /// when you're sure.
100    pub fn data_mut(&mut self) -> &mut D {
101        self.invalidated = true;
102        &mut self.data
103    }
104
105    /// Returns a non-invalidating reference to the managed data.
106    pub fn data(&self) -> &D {
107        &self.data
108    }
109
110    /// May apply some the managed sticky parameters to the given request.
111    ///
112    /// After receiving the response from this sliding sync, the caller MUST
113    /// also call [`Self::maybe_commit`] with the transaction id from the
114    /// server's response.
115    ///
116    /// If no `txn_id` is provided, it will generate one that can be reused
117    /// later.
118    pub fn maybe_apply(&mut self, req: &mut D::Request, txn_id: &mut LazyTransactionId) {
119        if self.invalidated {
120            let txn_id = txn_id.get_or_create();
121            self.txn_id = Some(txn_id.to_owned());
122            self.data.apply(req);
123        }
124    }
125
126    /// May mark the managed data as not invalidated anymore, if the transaction
127    /// id received from the response matches the one received from the request.
128    pub fn maybe_commit(&mut self, txn_id: &TransactionId) {
129        if self.invalidated && self.txn_id.as_deref() == Some(txn_id) {
130            self.invalidated = false;
131            self.data.on_commit();
132        }
133    }
134
135    #[cfg(test)]
136    pub fn is_invalidated(&self) -> bool {
137        self.invalidated
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::{LazyTransactionId, SlidingSyncStickyManager, StickyData};
144
145    struct EmptyStickyData(u8);
146
147    impl StickyData for EmptyStickyData {
148        type Request = bool;
149
150        fn apply(&self, req: &mut Self::Request) {
151            // Mark that applied has had an effect.
152            *req = true;
153        }
154
155        fn on_commit(&mut self) {
156            self.0 += 1;
157        }
158    }
159
160    #[test]
161    fn test_sticky_parameters_api_non_invalidated_no_effect() {
162        let mut sticky = SlidingSyncStickyManager::new(EmptyStickyData(0));
163
164        // At first, it's always invalidated.
165        assert!(sticky.is_invalidated());
166
167        let mut applied = false;
168        let mut txn_id = LazyTransactionId::new();
169        sticky.maybe_apply(&mut applied, &mut txn_id);
170        assert!(applied);
171        assert!(sticky.is_invalidated());
172        assert!(txn_id.get().is_some(), "a transaction id was lazily generated");
173
174        // Committing with the wrong transaction id won't commit.
175        sticky.maybe_commit("tid456".into());
176        assert_eq!(sticky.data.0, 0);
177        assert!(sticky.is_invalidated());
178
179        // Providing the correct transaction id will commit.
180        sticky.maybe_commit(txn_id.get().unwrap());
181        assert_eq!(sticky.data.0, 1);
182        assert!(!sticky.is_invalidated());
183
184        // Applying without being invalidated won't do anything, and not generate a
185        // transaction id.
186        let mut txn_id = LazyTransactionId::new();
187        let mut applied = false;
188        sticky.maybe_apply(&mut applied, &mut txn_id);
189
190        assert!(!applied);
191        assert!(!sticky.is_invalidated());
192        assert!(txn_id.get().is_none());
193    }
194}