atagen

matrix-rust-sdk

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/matrix-rust-sdk.git
crates/matrix-sdk-common/src/ttl.rsRust164 lines5.2 KB
1
// Copyright 2025 The Matrix.org Foundation C.I.C.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14
15
//! Types to implement TTL caches which can be used to persist data for a fixed
16
//! duration.
17
18
use ruma::time::SystemTime;
19
use serde::{Deserialize, Serialize};
20
21
/// A value that expires after some time.
22
///
23
/// This value is (de)serializable so it can be persisted in a store.
24
#[derive(Debug, Clone, Serialize, Deserialize)]
25
pub struct TtlValue<T> {
26
    /// The data of the item.
27
    #[serde(flatten)]
28
    data: T,
29
30
    /// Last time we fetched this data from the server, in milliseconds since
31
    /// UNIX epoch.
32
    ///
33
    /// When this field is missing during deserialization, it defaults to `0.0`,
34
    /// which means that the data is always expired. This allows to be
35
    /// compatible with data that was persisted before deciding to add an
36
    /// expiration time.
37
    #[serde(default = "default_timestamp")]
38
    last_fetch_ts: Option<f64>,
39
}
40
41
impl<T> TtlValue<T> {
42
    /// The number of milliseconds after which the data is considered stale.
43
    ///
44
    /// This matches 1 day.
45
    pub const STALE_THRESHOLD: f64 = (1000 * 60 * 60 * 24) as _;
46
47
    /// Construct a new `TtlValue` with the given data.
48
    pub fn new(data: T) -> Self {
49
        Self { data, last_fetch_ts: Some(now_timestamp_ms()) }
50
    }
51
52
    /// Construct a new `TtlValue` with the given data that never expires.
53
    pub fn without_expiry(data: T) -> Self {
54
        Self { data, last_fetch_ts: None }
55
    }
56
57
    /// Converts from `&TtlValue<T>` to `TtlValue<&T>`.
58
    pub fn as_ref(&self) -> TtlValue<&T> {
59
        TtlValue { data: &self.data, last_fetch_ts: self.last_fetch_ts }
60
    }
61
62
    /// Transform the data of this `TtlValue` with the given function.
63
    pub fn map<U, F>(self, f: F) -> TtlValue<U>
64
    where
65
        F: FnOnce(T) -> U,
66
    {
67
        TtlValue { data: f(self.data), last_fetch_ts: self.last_fetch_ts }
68
    }
69
70
    /// Whether this value has expired.
71
    pub fn has_expired(&self) -> bool {
72
        self.last_fetch_ts.is_some_and(|ts| now_timestamp_ms() - ts >= Self::STALE_THRESHOLD)
73
    }
74
75
    /// Mark this value has expired.
76
    pub fn expire(&mut self) {
77
        // We assume that the system time is always correct and we are far from the UNIX
78
        // epoch so a timestamp of 0 should always be expired.
79
        self.last_fetch_ts = Some(0.0)
80
    }
81
82
    /// Get a reference to the data of this value.
83
    pub fn data(&self) -> &T {
84
        &self.data
85
    }
86
87
    /// Get the data of this value.
88
    pub fn into_data(self) -> T {
89
        self.data
90
    }
91
}
92
93
/// Get the current timestamp as the number of milliseconds since Unix Epoch.
94
fn now_timestamp_ms() -> f64 {
95
    SystemTime::now()
96
        .duration_since(SystemTime::UNIX_EPOCH)
97
        .expect("System clock was before 1970.")
98
        .as_secs_f64()
99
        * 1000.0
100
}
101
102
/// The default timestamp if it is missing during deserialization.
103
///
104
/// We expect that a value that was serialized always has an expiry time, so the
105
/// default is `Some(0.0)`.
106
fn default_timestamp() -> Option<f64> {
107
    Some(0.0)
108
}
109
110
#[cfg(test)]
111
mod tests {
112
    use serde::{Deserialize, Serialize};
113
    use serde_json::json;
114
115
    use super::{TtlValue, now_timestamp_ms};
116
117
    #[test]
118
    fn test_ttl_value_expiry() {
119
        // Definitely stale.
120
        let ttl_value = TtlValue {
121
            data: (),
122
            last_fetch_ts: Some(now_timestamp_ms() - TtlValue::<()>::STALE_THRESHOLD - 1.0),
123
        };
124
        assert!(ttl_value.has_expired());
125
126
        // Definitely not stale.
127
        let ttl_value = TtlValue::new(());
128
        assert!(!ttl_value.has_expired());
129
130
        // Cannot be stale.
131
        let ttl_value = TtlValue::without_expiry(());
132
        assert!(!ttl_value.has_expired());
133
    }
134
135
    #[test]
136
    fn test_ttl_value_serialize_roundtrip() {
137
        #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
138
        struct Data {
139
            foo: String,
140
        }
141
142
        let data = Data { foo: "bar".to_owned() };
143
144
        // With timestamp.
145
        let ttl_value = TtlValue { data: data.clone(), last_fetch_ts: Some(1000.0) };
146
        let json = json!({
147
            "foo": "bar",
148
            "last_fetch_ts": 1000.0,
149
        });
150
        assert_eq!(serde_json::to_value(&ttl_value).unwrap(), json);
151
152
        let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
153
        assert_eq!(deserialized.data, data);
154
        assert!(deserialized.last_fetch_ts.unwrap() - ttl_value.last_fetch_ts.unwrap() < 0.0001);
155
156
        // Without timestamp the value is always expired in theory.
157
        let json = json!({
158
            "foo": "bar",
159
        });
160
        let deserialized = serde_json::from_value::<TtlValue<Data>>(json).unwrap();
161
        assert_eq!(deserialized.data, data);
162
        assert!(deserialized.last_fetch_ts.unwrap() - 0.0 < 0.0001);
163
    }
164
}