runecast_protocol/protocol/
envelope.rs

1//! Message envelope for reliable delivery and synchronization.
2//!
3//! Every message between client and server is wrapped in an envelope that provides:
4//! - Sequence numbers for ordering and acknowledgment
5//! - Timestamps for clock synchronization
6//! - Piggyback acknowledgments to reduce round trips
7//!
8//! # Wire Format
9//!
10//! The envelope is optional for backward compatibility. Messages can be sent
11//! either as raw payloads (legacy) or wrapped in envelopes (new protocol).
12//!
13//! ```json
14//! // Legacy format (still supported)
15//! { "type": "heartbeat" }
16//!
17//! // Envelope format
18//! {
19//!   "seq": 42,
20//!   "ack": 41,
21//!   "ts": 1701234567890,
22//!   "payload": { "type": "heartbeat" }
23//! }
24//! ```
25
26use serde::{Deserialize, Serialize};
27
28/// Message envelope wrapping any payload with delivery metadata.
29///
30/// Used for reliable message delivery with:
31/// - Sequence numbers for ordering
32/// - Acknowledgments for delivery confirmation
33/// - Timestamps for latency measurement and clock sync
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct Envelope<T> {
36    /// Monotonically increasing sequence number (per connection).
37    /// Server and client maintain separate sequences.
38    pub seq: u64,
39
40    /// Piggyback acknowledgment of the last received sequence number.
41    /// Allows confirming receipt without a separate ack message.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub ack: Option<u64>,
44
45    /// Server timestamp in milliseconds since Unix epoch.
46    /// Clients can use this for latency calculation and clock sync.
47    #[serde(rename = "ts")]
48    pub timestamp: u64,
49
50    /// The actual message payload.
51    pub payload: T,
52}
53
54impl<T> Envelope<T> {
55    /// Create a new envelope with the given sequence number and payload.
56    pub fn new(seq: u64, payload: T) -> Self {
57        Self {
58            seq,
59            ack: None,
60            timestamp: Self::now_millis(),
61            payload,
62        }
63    }
64
65    /// Create an envelope with a piggyback acknowledgment.
66    pub fn with_ack(seq: u64, ack: u64, payload: T) -> Self {
67        Self {
68            seq,
69            ack: Some(ack),
70            timestamp: Self::now_millis(),
71            payload,
72        }
73    }
74
75    /// Get current time in milliseconds since Unix epoch.
76    fn now_millis() -> u64 {
77        use std::time::{SystemTime, UNIX_EPOCH};
78        SystemTime::now()
79            .duration_since(UNIX_EPOCH)
80            .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(0))
81    }
82}
83
84impl<T> Envelope<T>
85where
86    T: Clone,
87{
88    /// Transform the payload while preserving envelope metadata.
89    pub fn map<U, F>(self, f: F) -> Envelope<U>
90    where
91        F: FnOnce(T) -> U,
92    {
93        Envelope {
94            seq: self.seq,
95            ack: self.ack,
96            timestamp: self.timestamp,
97            payload: f(self.payload),
98        }
99    }
100}
101
102/// Either an enveloped message or a raw payload (for backward compatibility).
103///
104/// During the migration period, clients may send either format.
105/// The server should accept both and respond in the same format.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(untagged)]
108pub enum MaybeEnveloped<T> {
109    /// New format with envelope
110    Enveloped(Envelope<T>),
111    /// Legacy format without envelope
112    Raw(T),
113}
114
115impl<T> MaybeEnveloped<T> {
116    /// Extract the payload regardless of format.
117    pub fn into_payload(self) -> T {
118        match self {
119            MaybeEnveloped::Enveloped(env) => env.payload,
120            MaybeEnveloped::Raw(payload) => payload,
121        }
122    }
123
124    /// Get sequence number if enveloped, None otherwise.
125    pub fn seq(&self) -> Option<u64> {
126        match self {
127            MaybeEnveloped::Enveloped(env) => Some(env.seq),
128            MaybeEnveloped::Raw(_) => None,
129        }
130    }
131
132    /// Get acknowledgment if enveloped, None otherwise.
133    pub fn ack(&self) -> Option<u64> {
134        match self {
135            MaybeEnveloped::Enveloped(env) => env.ack,
136            MaybeEnveloped::Raw(_) => None,
137        }
138    }
139
140    /// Check if this is an enveloped message.
141    pub fn is_enveloped(&self) -> bool {
142        matches!(self, MaybeEnveloped::Enveloped(_))
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use serde_json::json;
150
151    #[test]
152    fn test_envelope_serialization() {
153        let envelope = Envelope::new(42, json!({"type": "heartbeat"}));
154        let json = serde_json::to_string(&envelope).unwrap();
155
156        assert!(json.contains("\"seq\":42"));
157        assert!(json.contains("\"ts\":"));
158        assert!(json.contains("\"payload\""));
159    }
160
161    #[test]
162    fn test_envelope_with_ack() {
163        let envelope = Envelope::with_ack(42, 41, "test");
164        assert_eq!(envelope.seq, 42);
165        assert_eq!(envelope.ack, Some(41));
166    }
167
168    #[test]
169    fn test_maybe_enveloped_raw() {
170        let raw: MaybeEnveloped<String> = MaybeEnveloped::Raw("hello".to_string());
171        assert!(!raw.is_enveloped());
172        assert_eq!(raw.seq(), None);
173        assert_eq!(raw.into_payload(), "hello");
174    }
175
176    #[test]
177    fn test_maybe_enveloped_envelope() {
178        let env = MaybeEnveloped::Enveloped(Envelope::new(1, "hello".to_string()));
179        assert!(env.is_enveloped());
180        assert_eq!(env.seq(), Some(1));
181    }
182
183    #[test]
184    fn test_deserialize_raw_message() {
185        let json = r#"{"type": "heartbeat"}"#;
186        let result: MaybeEnveloped<serde_json::Value> = serde_json::from_str(json).unwrap();
187        assert!(!result.is_enveloped());
188    }
189
190    #[test]
191    fn test_deserialize_enveloped_message() {
192        let json = r#"{"seq": 1, "ts": 12345, "payload": {"type": "heartbeat"}}"#;
193        let result: MaybeEnveloped<serde_json::Value> = serde_json::from_str(json).unwrap();
194        assert!(result.is_enveloped());
195        assert_eq!(result.seq(), Some(1));
196    }
197}