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