atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git

Blame: crates/matrix-sdk-common/src/ttl.rs

7ff8adafe9e4atagen2026-10-22 12:30 UTC1
// Copyright 2025 The Matrix.org Foundation C.I.C.
7ff8adafe9e4atagen2026-10-22 12:30 UTC2
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC3
// Licensed under the Apache License, Version 2.0 (the "License");
7ff8adafe9e4atagen2026-10-22 12:30 UTC4
// you may not use this file except in compliance with the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC5
// You may obtain a copy of the License at
7ff8adafe9e4atagen2026-10-22 12:30 UTC6
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC7
//     http://www.apache.org/licenses/LICENSE-2.0
7ff8adafe9e4atagen2026-10-22 12:30 UTC8
//
7ff8adafe9e4atagen2026-10-22 12:30 UTC9
// Unless required by applicable law or agreed to in writing, software
7ff8adafe9e4atagen2026-10-22 12:30 UTC10
// distributed under the License is distributed on an "AS IS" BASIS,
7ff8adafe9e4atagen2026-10-22 12:30 UTC11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7ff8adafe9e4atagen2026-10-22 12:30 UTC12
// See the License for the specific language governing permissions and
7ff8adafe9e4atagen2026-10-22 12:30 UTC13
// limitations under the License.
7ff8adafe9e4atagen2026-10-22 12:30 UTC14
7ff8adafe9e4atagen2026-10-22 12:30 UTC15
//! Types to implement TTL caches which can be used to persist data for a fixed
7ff8adafe9e4atagen2026-10-22 12:30 UTC16
//! duration.
7ff8adafe9e4atagen2026-10-22 12:30 UTC17
7ff8adafe9e4atagen2026-10-22 12:30 UTC18
use ruma::time::SystemTime;
7ff8adafe9e4atagen2026-10-22 12:30 UTC19
use serde::{Deserialize, Serialize};
7ff8adafe9e4atagen2026-10-22 12:30 UTC20
7ff8adafe9e4atagen2026-10-22 12:30 UTC21
/// A value that expires after some time.
7ff8adafe9e4atagen2026-10-22 12:30 UTC22
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC23
/// This value is (de)serializable so it can be persisted in a store.
7ff8adafe9e4atagen2026-10-22 12:30 UTC24
#[derive(Debug, Clone, Serialize, Deserialize)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC25
pub struct TtlValue<T> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC26
    /// The data of the item.
7ff8adafe9e4atagen2026-10-22 12:30 UTC27
    #[serde(flatten)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC28
    data: T,
7ff8adafe9e4atagen2026-10-22 12:30 UTC29
7ff8adafe9e4atagen2026-10-22 12:30 UTC30
    /// Last time we fetched this data from the server, in milliseconds since
7ff8adafe9e4atagen2026-10-22 12:30 UTC31
    /// UNIX epoch.
7ff8adafe9e4atagen2026-10-22 12:30 UTC32
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC33
    /// When this field is missing during deserialization, it defaults to `0.0`,
7ff8adafe9e4atagen2026-10-22 12:30 UTC34
    /// which means that the data is always expired. This allows to be
7ff8adafe9e4atagen2026-10-22 12:30 UTC35
    /// compatible with data that was persisted before deciding to add an
7ff8adafe9e4atagen2026-10-22 12:30 UTC36
    /// expiration time.
7ff8adafe9e4atagen2026-10-22 12:30 UTC37
    #[serde(default = "default_timestamp")]
7ff8adafe9e4atagen2026-10-22 12:30 UTC38
    last_fetch_ts: Option<f64>,
7ff8adafe9e4atagen2026-10-22 12:30 UTC39
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC40
7ff8adafe9e4atagen2026-10-22 12:30 UTC41
impl<T> TtlValue<T> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC42
    /// The number of milliseconds after which the data is considered stale.
7ff8adafe9e4atagen2026-10-22 12:30 UTC43
    ///
7ff8adafe9e4atagen2026-10-22 12:30 UTC44
    /// This matches 1 day.
7ff8adafe9e4atagen2026-10-22 12:30 UTC45
    pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24) as _;
7ff8adafe9e4atagen2026-10-22 12:30 UTC46
7ff8adafe9e4atagen2026-10-22 12:30 UTC47
    /// Construct a new `TtlValue` with the given data.
7ff8adafe9e4atagen2026-10-22 12:30 UTC48
    pub fn new(data: T) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC49
        Self { data, last_fetch_ts: Some(now_timestamp_ms()) }
7ff8adafe9e4atagen2026-10-22 12:30 UTC50
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC51
7ff8adafe9e4atagen2026-10-22 12:30 UTC52
    /// Construct a new `TtlValue` with the given data that never expires.
7ff8adafe9e4atagen2026-10-22 12:30 UTC53
    pub fn without_expiry(data: T) -> Self {
7ff8adafe9e4atagen2026-10-22 12:30 UTC54
        Self { data, last_fetch_ts: None }
7ff8adafe9e4atagen2026-10-22 12:30 UTC55
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC56
7ff8adafe9e4atagen2026-10-22 12:30 UTC57
    /// Converts from `&TtlValue<T>` to `TtlValue<&T>`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC58
    pub fn as_ref(&self) -> TtlValue<&T> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC59
        TtlValue { data: &self.data, last_fetch_ts: self.last_fetch_ts }
7ff8adafe9e4atagen2026-10-22 12:30 UTC60
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC61
7ff8adafe9e4atagen2026-10-22 12:30 UTC62
    /// Transform the data of this `TtlValue` with the given function.
7ff8adafe9e4atagen2026-10-22 12:30 UTC63
    pub fn map<U, F>(self, f: F) -> TtlValue<U>
7ff8adafe9e4atagen2026-10-22 12:30 UTC64
    where
7ff8adafe9e4atagen2026-10-22 12:30 UTC65
        F: FnOnce(T) -> U,
7ff8adafe9e4atagen2026-10-22 12:30 UTC66
    {
7ff8adafe9e4atagen2026-10-22 12:30 UTC67
        TtlValue { data: f(self.data), last_fetch_ts: self.last_fetch_ts }
7ff8adafe9e4atagen2026-10-22 12:30 UTC68
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC69
7ff8adafe9e4atagen2026-10-22 12:30 UTC70
    /// Whether this value has expired.
7ff8adafe9e4atagen2026-10-22 12:30 UTC71
    pub fn has_expired(&self) -> bool {
7ff8adafe9e4atagen2026-10-22 12:30 UTC72
        self.last_fetch_ts.is_some_and(|ts| now_timestamp_ms() - ts >= Self::STALE_THRESHOLD)
7ff8adafe9e4atagen2026-10-22 12:30 UTC73
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC74
7ff8adafe9e4atagen2026-10-22 12:30 UTC75
    /// Mark this value has expired.
7ff8adafe9e4atagen2026-10-22 12:30 UTC76
    pub fn expire(&mut self) {
7ff8adafe9e4atagen2026-10-22 12:30 UTC77
        // We assume that the system time is always correct and we are far from the UNIX
7ff8adafe9e4atagen2026-10-22 12:30 UTC78
        // epoch so a timestamp of 0 should always be expired.
7ff8adafe9e4atagen2026-10-22 12:30 UTC79
        self.last_fetch_ts = Some(0.0)
7ff8adafe9e4atagen2026-10-22 12:30 UTC80
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC81
7ff8adafe9e4atagen2026-10-22 12:30 UTC82
    /// Get a reference to the data of this value.
7ff8adafe9e4atagen2026-10-22 12:30 UTC83
    pub fn data(&self) -> &T {
7ff8adafe9e4atagen2026-10-22 12:30 UTC84
        &self.data
7ff8adafe9e4atagen2026-10-22 12:30 UTC85
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC86
7ff8adafe9e4atagen2026-10-22 12:30 UTC87
    /// Get the data of this value.
7ff8adafe9e4atagen2026-10-22 12:30 UTC88
    pub fn into_data(self) -> T {
7ff8adafe9e4atagen2026-10-22 12:30 UTC89
        self.data
7ff8adafe9e4atagen2026-10-22 12:30 UTC90
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC91
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC92
7ff8adafe9e4atagen2026-10-22 12:30 UTC93
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
7ff8adafe9e4atagen2026-10-22 12:30 UTC94
fn now_timestamp_ms() -> f64 {
7ff8adafe9e4atagen2026-10-22 12:30 UTC95
    SystemTime::now()
7ff8adafe9e4atagen2026-10-22 12:30 UTC96
        .duration_since(SystemTime::UNIX_EPOCH)
7ff8adafe9e4atagen2026-10-22 12:30 UTC97
        .expect("System clock was before 1970.")
7ff8adafe9e4atagen2026-10-22 12:30 UTC98
        .as_secs_f64()
7ff8adafe9e4atagen2026-10-22 12:30 UTC99
        * 1000.0
7ff8adafe9e4atagen2026-10-22 12:30 UTC100
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC101
7ff8adafe9e4atagen2026-10-22 12:30 UTC102
/// The default timestamp if it is missing during deserialization.
7ff8adafe9e4atagen2026-10-22 12:30 UTC103
///
7ff8adafe9e4atagen2026-10-22 12:30 UTC104
/// We expect that a value that was serialized always has an expiry time, so the
7ff8adafe9e4atagen2026-10-22 12:30 UTC105
/// default is `Some(0.0)`.
7ff8adafe9e4atagen2026-10-22 12:30 UTC106
fn default_timestamp() -> Option<f64> {
7ff8adafe9e4atagen2026-10-22 12:30 UTC107
    Some(0.0)
7ff8adafe9e4atagen2026-10-22 12:30 UTC108
}
7ff8adafe9e4atagen2026-10-22 12:30 UTC109
7ff8adafe9e4atagen2026-10-22 12:30 UTC110
#[cfg(test)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC111
mod tests {
7ff8adafe9e4atagen2026-10-22 12:30 UTC112
    use serde::{Deserialize, Serialize};
7ff8adafe9e4atagen2026-10-22 12:30 UTC113
    use serde_json::json;
7ff8adafe9e4atagen2026-10-22 12:30 UTC114
7ff8adafe9e4atagen2026-10-22 12:30 UTC115
    use super::{TtlValue, now_timestamp_ms};
7ff8adafe9e4atagen2026-10-22 12:30 UTC116
7ff8adafe9e4atagen2026-10-22 12:30 UTC117
    #[test]
7ff8adafe9e4atagen2026-10-22 12:30 UTC118
    fn test_ttl_value_expiry() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC119
        // Definitely stale.
7ff8adafe9e4atagen2026-10-22 12:30 UTC120
        let ttl_value = TtlValue {
7ff8adafe9e4atagen2026-10-22 12:30 UTC121
            data: (),
7ff8adafe9e4atagen2026-10-22 12:30 UTC122
            last_fetch_ts: Some(now_timestamp_ms() - TtlValue::<()>::STALE_THRESHOLD - 1.0),
7ff8adafe9e4atagen2026-10-22 12:30 UTC123
        };
7ff8adafe9e4atagen2026-10-22 12:30 UTC124
        assert!(ttl_value.has_expired());
7ff8adafe9e4atagen2026-10-22 12:30 UTC125
7ff8adafe9e4atagen2026-10-22 12:30 UTC126
        // Definitely not stale.
7ff8adafe9e4atagen2026-10-22 12:30 UTC127
        let ttl_value = TtlValue::new(());
7ff8adafe9e4atagen2026-10-22 12:30 UTC128
        assert!(!ttl_value.has_expired());
7ff8adafe9e4atagen2026-10-22 12:30 UTC129
7ff8adafe9e4atagen2026-10-22 12:30 UTC130
        // Cannot be stale.
7ff8adafe9e4atagen2026-10-22 12:30 UTC131
        let ttl_value = TtlValue::without_expiry(());
7ff8adafe9e4atagen2026-10-22 12:30 UTC132
        assert!(!ttl_value.has_expired());
7ff8adafe9e4atagen2026-10-22 12:30 UTC133
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC134
7ff8adafe9e4atagen2026-10-22 12:30 UTC135
    #[test]
7ff8adafe9e4atagen2026-10-22 12:30 UTC136
    fn test_ttl_value_serialize_roundtrip() {
7ff8adafe9e4atagen2026-10-22 12:30 UTC137
        #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7ff8adafe9e4atagen2026-10-22 12:30 UTC138
        struct Data {
7ff8adafe9e4atagen2026-10-22 12:30 UTC139
            foo: String,
7ff8adafe9e4atagen2026-10-22 12:30 UTC140
        }
7ff8adafe9e4atagen2026-10-22 12:30 UTC141
7ff8adafe9e4atagen2026-10-22 12:30 UTC142
        let data = Data { foo: "bar".to_owned() };
7ff8adafe9e4atagen2026-10-22 12:30 UTC143
7ff8adafe9e4atagen2026-10-22 12:30 UTC144
        // With timestamp.
7ff8adafe9e4atagen2026-10-22 12:30 UTC145
        let ttl_value = TtlValue { data: data.clone(), last_fetch_ts: Some(1000.0) };
7ff8adafe9e4atagen2026-10-22 12:30 UTC146
        let json = json!({
7ff8adafe9e4atagen2026-10-22 12:30 UTC147
            "foo": "bar",
7ff8adafe9e4atagen2026-10-22 12:30 UTC148
            "last_fetch_ts": 1000.0,
7ff8adafe9e4atagen2026-10-22 12:30 UTC149
        });
7ff8adafe9e4atagen2026-10-22 12:30 UTC150
        assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
7ff8adafe9e4atagen2026-10-22 12:30 UTC151
7ff8adafe9e4atagen2026-10-22 12:30 UTC152
        let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC153
        assert_eq!(deserialized.data, data);
7ff8adafe9e4atagen2026-10-22 12:30 UTC154
        assert!(deserialized.last_fetch_ts.unwrap() - ttl_value.last_fetch_ts.unwrap() < 0.0001);
7ff8adafe9e4atagen2026-10-22 12:30 UTC155
7ff8adafe9e4atagen2026-10-22 12:30 UTC156
        // Without timestamp the value is always expired in theory.
7ff8adafe9e4atagen2026-10-22 12:30 UTC157
        let json = json!({
7ff8adafe9e4atagen2026-10-22 12:30 UTC158
            "foo": "bar",
7ff8adafe9e4atagen2026-10-22 12:30 UTC159
        });
7ff8adafe9e4atagen2026-10-22 12:30 UTC160
        let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
7ff8adafe9e4atagen2026-10-22 12:30 UTC161
        assert_eq!(deserialized.data, data);
7ff8adafe9e4atagen2026-10-22 12:30 UTC162
        assert!(deserialized.last_fetch_ts.unwrap() - 0.0 < 0.0001);
7ff8adafe9e4atagen2026-10-22 12:30 UTC163
    }
7ff8adafe9e4atagen2026-10-22 12:30 UTC164
}