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}