Skip to main content

truffle_core/envelope/
mod.rs

1//! Layer 6: Envelope — Namespace-routed message framing.
2//!
3//! All application messages exchanged over WebSocket connections are wrapped
4//! in an [`Envelope`]. The envelope carries a namespace string that routes
5//! messages to the correct application subscriber, and an opaque JSON payload
6//! that truffle-core **never** inspects.
7//!
8//! # Layer rules
9//!
10//! - Layer 6 defines the envelope format, serialization, and deserialization
11//! - Layer 6 does NOT route messages — that is the Node/Session layer's job
12//! - Layer 6 does NOT know about WebSocket, TCP, or any transport
13//! - The `namespace` field is an opaque string — Layer 6 never matches known values
14//! - No `"mesh"`, `"device-announce"`, or `"file-transfer"` constants here
15
16pub mod codec;
17
18use serde::{Deserialize, Serialize};
19
20// Re-export the codec types for convenience.
21pub use codec::{EnvelopeCodec, JsonCodec};
22
23// ---------------------------------------------------------------------------
24// Envelope
25// ---------------------------------------------------------------------------
26
27/// A namespace-routed message. All application messages use this format
28/// over WebSocket connections. truffle-core NEVER inspects the payload.
29///
30/// # Fields
31///
32/// - `namespace`: Application-owned routing key (e.g., `"ft"`, `"chat"`).
33/// - `msg_type`: Application-defined message type within the namespace.
34/// - `payload`: Opaque JSON value. truffle-core never deserializes this.
35/// - `timestamp`: Optional millisecond Unix timestamp, set by the sender.
36/// - `from`: Sender peer ID, typically set by the receiving side (Layer 5).
37///
38/// # Forward compatibility
39///
40/// Unknown fields in the JSON representation are silently ignored during
41/// deserialization, allowing older nodes to receive messages from newer
42/// protocol versions without error.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Envelope {
45    /// Namespace owned by the application (e.g., `"ft"`, `"chat"`, `"bus"`).
46    /// Used for routing to the correct subscriber.
47    pub namespace: String,
48
49    /// Application-defined message type within the namespace.
50    pub msg_type: String,
51
52    /// Opaque payload. truffle-core never deserializes this further.
53    pub payload: serde_json::Value,
54
55    /// Millisecond Unix timestamp, set by the sender.
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub timestamp: Option<u64>,
58
59    /// Sender peer ID, typically populated on the receiving side.
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub from: Option<String>,
62}
63
64impl Envelope {
65    /// Create a new envelope with the given namespace, message type, and payload.
66    ///
67    /// The `timestamp` and `from` fields are left as `None`. Use
68    /// [`with_timestamp`](Self::with_timestamp) to stamp the current time.
69    pub fn new(namespace: &str, msg_type: &str, payload: serde_json::Value) -> Self {
70        Self {
71            namespace: namespace.to_string(),
72            msg_type: msg_type.to_string(),
73            payload,
74            timestamp: None,
75            from: None,
76        }
77    }
78
79    /// Set the timestamp to the current Unix time in milliseconds.
80    pub fn with_timestamp(mut self) -> Self {
81        let now = std::time::SystemTime::now()
82            .duration_since(std::time::UNIX_EPOCH)
83            .unwrap_or_default()
84            .as_millis() as u64;
85        self.timestamp = Some(now);
86        self
87    }
88
89    /// Serialize this envelope to JSON bytes using the default codec.
90    pub fn serialize(&self) -> Result<Vec<u8>, EnvelopeError> {
91        serde_json::to_vec(self).map_err(EnvelopeError::Serialization)
92    }
93
94    /// Deserialize an envelope from JSON bytes using the default codec.
95    pub fn deserialize(data: &[u8]) -> Result<Self, EnvelopeError> {
96        serde_json::from_slice(data).map_err(EnvelopeError::Deserialization)
97    }
98}
99
100// ---------------------------------------------------------------------------
101// Errors
102// ---------------------------------------------------------------------------
103
104/// Errors from Layer 6 envelope operations.
105#[derive(Debug, thiserror::Error)]
106pub enum EnvelopeError {
107    /// Failed to serialize an envelope.
108    #[error("envelope serialization error: {0}")]
109    Serialization(serde_json::Error),
110
111    /// Failed to deserialize an envelope.
112    #[error("envelope deserialization error: {0}")]
113    Deserialization(serde_json::Error),
114}
115
116// ---------------------------------------------------------------------------
117// Tests
118// ---------------------------------------------------------------------------
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use serde_json::json;
124
125    #[test]
126    fn test_envelope_serialize_deserialize_roundtrip() {
127        let envelope = Envelope::new("ft", "offer", json!({"file": "test.txt", "size": 1024}))
128            .with_timestamp();
129
130        let bytes = envelope.serialize().expect("serialize");
131        let decoded = Envelope::deserialize(&bytes).expect("deserialize");
132
133        assert_eq!(decoded.namespace, "ft");
134        assert_eq!(decoded.msg_type, "offer");
135        assert_eq!(decoded.payload["file"], "test.txt");
136        assert_eq!(decoded.payload["size"], 1024);
137        assert!(decoded.timestamp.is_some());
138        assert_eq!(decoded.timestamp, envelope.timestamp);
139    }
140
141    #[test]
142    fn test_envelope_timestamp_generation() {
143        let before = std::time::SystemTime::now()
144            .duration_since(std::time::UNIX_EPOCH)
145            .unwrap()
146            .as_millis() as u64;
147
148        let envelope = Envelope::new("test", "ping", json!(null)).with_timestamp();
149
150        let after = std::time::SystemTime::now()
151            .duration_since(std::time::UNIX_EPOCH)
152            .unwrap()
153            .as_millis() as u64;
154
155        let ts = envelope.timestamp.expect("timestamp should be set");
156        assert!(ts >= before, "timestamp {ts} should be >= {before}");
157        assert!(ts <= after, "timestamp {ts} should be <= {after}");
158    }
159
160    #[test]
161    fn test_envelope_unknown_fields_ignored() {
162        // Simulate a future protocol version that adds an extra field.
163        let json_str = r#"{
164            "namespace": "chat",
165            "msg_type": "message",
166            "payload": {"text": "hello"},
167            "timestamp": 1234567890,
168            "from": "peer-1",
169            "future_field": "should be ignored",
170            "another_new_field": 42
171        }"#;
172
173        let envelope: Envelope =
174            serde_json::from_str(json_str).expect("should ignore unknown fields");
175        assert_eq!(envelope.namespace, "chat");
176        assert_eq!(envelope.msg_type, "message");
177        assert_eq!(envelope.payload["text"], "hello");
178        assert_eq!(envelope.timestamp, Some(1234567890));
179        assert_eq!(envelope.from.as_deref(), Some("peer-1"));
180    }
181
182    #[test]
183    fn test_envelope_empty_payload() {
184        let envelope = Envelope::new("bus", "heartbeat", json!(null));
185        let bytes = envelope.serialize().expect("serialize");
186        let decoded = Envelope::deserialize(&bytes).expect("deserialize");
187
188        assert_eq!(decoded.namespace, "bus");
189        assert_eq!(decoded.msg_type, "heartbeat");
190        assert!(decoded.payload.is_null());
191        assert!(decoded.timestamp.is_none());
192        assert!(decoded.from.is_none());
193    }
194
195    #[test]
196    fn test_envelope_large_payload() {
197        // ~100 KB payload
198        let large_data: Vec<u8> = (0..100_000).map(|i| (i % 256) as u8).collect();
199        let payload = json!({"data": large_data});
200
201        let envelope = Envelope::new("stream", "chunk", payload.clone()).with_timestamp();
202        let bytes = envelope.serialize().expect("serialize");
203        let decoded = Envelope::deserialize(&bytes).expect("deserialize");
204
205        assert_eq!(decoded.namespace, "stream");
206        assert_eq!(decoded.msg_type, "chunk");
207        assert_eq!(decoded.payload, payload);
208    }
209
210    #[test]
211    fn test_envelope_optional_fields_not_serialized_when_none() {
212        let envelope = Envelope::new("test", "msg", json!("data"));
213
214        let json_str = serde_json::to_string(&envelope).expect("serialize");
215        assert!(!json_str.contains("timestamp"), "None timestamp should be skipped");
216        assert!(!json_str.contains("from"), "None from should be skipped");
217    }
218
219    #[test]
220    fn test_envelope_deserialization_error() {
221        let bad_json = b"not valid json";
222        let result = Envelope::deserialize(bad_json);
223        assert!(result.is_err());
224    }
225}