Skip to main content

ferogram_msgbox/
defs.rs

1/*
2 * Copyright (c) 2026 Ankit Chaubey <ankitchaubey.dev@gmail.com>
3 * https://github.com/ankit-chaubey
4 *
5 * Project: ferogram
6 * Website: https://ferogram.dev
7 *
8 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
9 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
10 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
11 * This file may not be copied, modified, or distributed except according
12 * to those terms.
13 */
14
15use std::time::Duration;
16
17// Instant (real vs test-time fake)
18
19#[cfg(not(test))]
20pub(super) use std::time::Instant;
21
22/// Thread-local fake clock used in tests so we never need `thread::sleep`.
23///
24/// The production code in mod.rs simply calls `Instant::now()`; under
25/// `cfg(test)` that resolves to the controlled fake below.
26#[cfg(test)]
27pub(super) mod fake_clock {
28    use std::cell::RefCell;
29    use std::ops::Add;
30    use std::time::Duration;
31
32    thread_local! {
33        static NOW: RefCell<Duration> = const { RefCell::new(Duration::ZERO) };
34    }
35
36    /// A fake `Instant` backed by a thread-local counter.
37    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
38    pub struct Instant(pub Duration);
39
40    impl Instant {
41        pub fn now() -> Self {
42            NOW.with_borrow(|d| Self(*d))
43        }
44
45        /// Helper used only in tests to observe the raw duration.
46        pub fn elapsed_secs(&self) -> u64 {
47            self.0.as_secs()
48        }
49    }
50
51    impl Add<Duration> for Instant {
52        type Output = Instant;
53        fn add(self, rhs: Duration) -> Instant {
54            Instant(self.0 + rhs)
55        }
56    }
57
58    /// Reset the fake clock to zero.  Call at the start of every test.
59    pub fn reset_time() {
60        NOW.with_borrow_mut(|d| *d = Duration::ZERO);
61    }
62
63    /// Advance the fake clock by `dur`.
64    pub fn advance_time_by(dur: Duration) {
65        NOW.with_borrow_mut(|d| *d += dur);
66    }
67}
68
69#[cfg(test)]
70pub(super) use fake_clock::Instant;
71
72// Allow reader_loop code (which calls `check_deadlines().into()`) to compile
73// under `cfg(test)`.  The reader loop never actually runs in unit-tests, so
74// this conversion just returns a reasonable stand-in.
75#[cfg(test)]
76impl From<fake_clock::Instant> for tokio::time::Instant {
77    fn from(i: fake_clock::Instant) -> Self {
78        tokio::time::Instant::now()
79            .checked_add(i.0)
80            .unwrap_or_else(tokio::time::Instant::now)
81    }
82}
83
84use ferogram_tl_types as tl;
85
86/// Telegram sends `seq` equal to `0` when "it doesn't matter", so we use that value too.
87pub(super) const NO_SEQ: i32 = 0;
88
89/// `qts` of `0` means "ordering should be ignored" for that update.
90pub(super) const NO_PTS: i32 = 0;
91
92/// Sentinel `date` value when constructing dummy Updates containers.
93pub(super) const NO_DATE: i32 = 0;
94
95/// Wait up to 0.5 s before declaring a gap a real gap.
96pub(super) const POSSIBLE_GAP_TIMEOUT: Duration = Duration::from_millis(500);
97
98/// After how long without updates the client will proactively fetch updates.
99///
100/// Documentation recommends 15 minutes without updates.
101pub(super) const NO_UPDATES_TIMEOUT: Duration = Duration::from_secs(15 * 60);
102
103// Keys
104
105/// A sortable message-box entry key.
106#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
107pub(crate) enum Key {
108    Common,
109    Secondary,
110    Channel(i64),
111}
112
113// Live entry
114
115/// A single live entry inside [`MessageBoxes`].
116#[derive(Debug)]
117pub(super) struct LiveEntry {
118    pub(super) key: Key,
119    pub(super) pts: i32,
120    /// Next instant when we forcibly fetch difference if no updates arrived by then.
121    pub(super) deadline: Instant,
122    /// If set, we detected a possible gap and are waiting to see if it resolves itself.
123    pub(super) possible_gap: Option<PossibleGap>,
124}
125
126impl LiveEntry {
127    pub(super) fn effective_deadline(&self) -> Instant {
128        match &self.possible_gap {
129            Some(gap) => gap.deadline.min(self.deadline),
130            None => self.deadline,
131        }
132    }
133}
134
135// PossibleGap
136
137#[derive(Debug)]
138pub(super) struct PossibleGap {
139    pub(super) deadline: Instant,
140    /// Pending updates (those with a higher pts that are creating the gap).
141    pub(super) updates: Vec<tl::enums::Update>,
142}
143
144// MessageBoxes (container)
145
146/// All live message boxes.  The single authority for update-gap detection.
147///
148/// See <https://core.telegram.org/api/updates#message-related-event-sequences>.
149#[derive(Debug)]
150pub struct MessageBoxes {
151    /// Live entries sorted by key.
152    pub(super) entries: Vec<LiveEntry>,
153
154    pub(super) date: i32,
155    pub(super) seq: i32,
156
157    /// Entries for which we must currently fetch difference.
158    pub(super) getting_diff_for: Vec<Key>,
159
160    /// Channel diff request currently in flight (RPC sent, response not yet
161    /// applied).  Distinct from `getting_diff_for`, which controls update
162    /// suppression and survives across the RTT.  This flag prevents
163    /// `get_channel_difference()` from handing out a second concurrent
164    /// request for the same channel (e.g. across a reconnect generation
165    /// boundary), and is cleared whenever the response (or its premature-end
166    /// equivalent) is consumed - by `apply_channel_difference` or
167    /// `end_channel_difference` - regardless of which generation issued it.
168    pub(super) channel_diff_in_flight: Option<i64>,
169
170    /// Cached minimum deadline across all entries.
171    pub(super) next_deadline: Instant,
172}
173
174// PtsInfo - per-update pts metadata
175
176#[derive(Debug)]
177pub(super) struct PtsInfo {
178    pub(super) key: Key,
179    pub(super) pts: i32,
180    pub(super) count: i32,
181}
182
183// Gap error
184
185/// Returned by [`MessageBoxes::process_updates`] when a gap is detected.
186#[derive(Debug, PartialEq, Eq)]
187pub struct Gap;
188
189// UpdatesLike
190
191/// Anything that should be treated as an update batch.
192#[derive(Debug)]
193pub enum UpdatesLike {
194    /// Normal push update from the socket.
195    Updates(Box<tl::enums::Updates>),
196    /// The connection was closed; a gap may now exist.
197    ConnectionClosed,
198    /// A received update could not be parsed (unknown constructor, truncation).
199    MalformedUpdates,
200    /// RPC response for `messages.deleteMessages` / `messages.readHistory` etc.
201    AffectedMessages(tl::types::messages::AffectedMessages),
202    /// Same as above but channel-specific.
203    AffectedChannelMessages {
204        affected: tl::types::messages::AffectedMessages,
205        channel_id: i64,
206    },
207    /// updateShortSentMessage confirmed; request_body used to reconstruct the outgoing message.
208    /// If body is None or not a SendMessage, advances pts silently.
209    SentMessage {
210        pts: i32,
211        pts_count: i32,
212        request_body: Option<Vec<u8>>,
213        update: Box<tl::types::UpdateShortSentMessage>,
214    },
215}
216
217// Public update state types (for persisting)
218
219/// Per-channel pts snapshot.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct ChannelState {
222    pub id: i64,
223    pub pts: i32,
224}
225
226/// Full snapshot of the update state for session persistence.
227#[derive(Debug, Clone, Default, PartialEq, Eq)]
228pub struct UpdatesStateSnap {
229    pub pts: i32,
230    pub qts: i32,
231    pub date: i32,
232    pub seq: i32,
233    pub channels: Vec<ChannelState>,
234}
235
236// Pair type
237
238pub type UpdateAndPeers = (
239    Vec<tl::enums::Update>,
240    Vec<tl::enums::User>,
241    Vec<tl::enums::Chat>,
242);