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);