1use std::time::Duration;
27
28use tokio::sync::broadcast;
29
30use crate::network::NetworkProvider;
31use crate::node::{NamespacedMessage, Node, NodeError};
32
33#[derive(Debug, thiserror::Error)]
35pub enum RequestError {
36 #[error("send failed: {0}")]
38 Send(NodeError),
39 #[error("timed out waiting for reply")]
41 Timeout,
42 #[error("subscription channel closed")]
44 ChannelClosed,
45}
46
47pub async fn send_and_wait<N, F, R>(
57 node: &Node<N>,
58 peer_id: &str,
59 namespace: &str,
60 msg_type: &str,
61 payload: &serde_json::Value,
62 timeout: Duration,
63 filter: F,
64) -> Result<R, RequestError>
65where
66 N: NetworkProvider + 'static,
67 F: Fn(&NamespacedMessage) -> Option<R>,
68{
69 let mut rx = node.subscribe(namespace);
71
72 node.send_typed(peer_id, namespace, msg_type, payload)
73 .await
74 .map_err(RequestError::Send)?;
75
76 let deadline = tokio::time::Instant::now() + timeout;
77
78 loop {
79 let remaining = deadline.duration_since(tokio::time::Instant::now());
80 if remaining.is_zero() {
81 return Err(RequestError::Timeout);
82 }
83
84 match tokio::time::timeout(remaining, rx.recv()).await {
85 Ok(Ok(msg)) => {
86 if let Some(result) = filter(&msg) {
87 return Ok(result);
88 }
89 }
91 Ok(Err(broadcast::error::RecvError::Lagged(n))) => {
92 tracing::warn!(missed = n, "request_reply: receiver lagged");
93 }
95 Ok(Err(broadcast::error::RecvError::Closed)) => {
96 return Err(RequestError::ChannelClosed);
97 }
98 Err(_elapsed) => {
99 return Err(RequestError::Timeout);
100 }
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::sync::Arc;
109 use tokio::time::Duration;
110
111 use crate::envelope::codec::JsonCodec;
112 use crate::envelope::EnvelopeCodec;
113 use crate::network::*;
114 use crate::session::PeerRegistry;
115 use crate::transport::websocket::WebSocketTransport;
116 use crate::transport::WsConfig;
117
118 struct MockNetworkProvider {
121 identity: NodeIdentity,
122 local_addr: PeerAddr,
123 peer_event_tx: tokio::sync::broadcast::Sender<NetworkPeerEvent>,
124 mock_peers: Arc<tokio::sync::RwLock<Vec<NetworkPeer>>>,
125 }
126
127 impl MockNetworkProvider {
128 fn new(id: &str) -> Self {
129 let (peer_event_tx, _) = tokio::sync::broadcast::channel(64);
130 Self {
131 identity: NodeIdentity {
132 app_id: "test".to_string(),
133 device_id: id.to_string(),
135 device_name: format!("Test Node {id}"),
136 tailscale_hostname: format!("truffle-test-{id}"),
137 tailscale_id: id.to_string(),
138 dns_name: None,
139 ip: Some("127.0.0.1".parse().unwrap()),
140 },
141 local_addr: PeerAddr {
142 ip: Some("127.0.0.1".parse().unwrap()),
143 hostname: format!("truffle-test-{id}"),
144 dns_name: None,
145 },
146 peer_event_tx,
147 mock_peers: Arc::new(tokio::sync::RwLock::new(Vec::new())),
148 }
149 }
150
151 fn event_sender(&self) -> tokio::sync::broadcast::Sender<NetworkPeerEvent> {
152 self.peer_event_tx.clone()
153 }
154 }
155
156 impl NetworkProvider for MockNetworkProvider {
157 fn local_identity(&self) -> NodeIdentity {
158 self.identity.clone()
159 }
160 fn local_addr(&self) -> PeerAddr {
161 self.local_addr.clone()
162 }
163 fn peer_events(&self) -> tokio::sync::broadcast::Receiver<NetworkPeerEvent> {
164 self.peer_event_tx.subscribe()
165 }
166
167 async fn start(&mut self) -> Result<(), NetworkError> {
168 Ok(())
169 }
170 async fn stop(&self) -> Result<(), NetworkError> {
171 Ok(())
172 }
173 async fn peers(&self) -> Vec<NetworkPeer> {
174 self.mock_peers.read().await.clone()
175 }
176 async fn dial_tcp(
177 &self,
178 _addr: &str,
179 _port: u16,
180 ) -> Result<tokio::net::TcpStream, NetworkError> {
181 Err(NetworkError::DialFailed("mock".into()))
182 }
183 async fn listen_tcp(&self, _port: u16) -> Result<NetworkTcpListener, NetworkError> {
184 Err(NetworkError::ListenFailed("mock".into()))
185 }
186 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> {
187 Ok(())
188 }
189 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
190 Err(NetworkError::NotRunning)
191 }
192 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
193 Ok(PingResult {
194 latency: Duration::from_millis(1),
195 connection: "direct".to_string(),
196 peer_addr: None,
197 })
198 }
199 async fn health(&self) -> HealthInfo {
200 HealthInfo {
201 state: "running".to_string(),
202 healthy: true,
203 ..Default::default()
204 }
205 }
206 }
207
208 fn ws_config(port: u16) -> WsConfig {
209 WsConfig {
210 port,
211 ping_interval: Duration::from_secs(300),
212 pong_timeout: Duration::from_secs(300),
213 ..Default::default()
214 }
215 }
216
217 async fn random_port() -> u16 {
218 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
219 l.local_addr().unwrap().port()
220 }
221
222 fn make_loopback_peer(id: &str) -> NetworkPeer {
223 NetworkPeer {
224 id: id.to_string(),
225 hostname: format!("truffle-test-{id}"),
226 ip: "127.0.0.1".parse().unwrap(),
227 online: true,
228 cur_addr: Some("127.0.0.1:41641".to_string()),
229 relay: None,
230 os: Some("linux".to_string()),
231 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
232 key_expiry: None,
233 dns_name: None,
234 }
235 }
236
237 async fn make_test_node(
238 id: &str,
239 ws_port: u16,
240 ) -> (
241 Node<MockNetworkProvider>,
242 tokio::sync::broadcast::Sender<NetworkPeerEvent>,
243 ) {
244 let provider = MockNetworkProvider::new(id);
245 let event_tx = provider.event_sender();
246 let network = Arc::new(provider);
247 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
248 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
249 session.start().await;
250
251 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
252 let node = Node::from_parts(network, session, codec);
253 (node, event_tx)
254 }
255
256 #[tokio::test]
257 async fn test_send_and_wait_timeout() {
258 let ws_port = random_port().await;
259 let (node, event_tx) = make_test_node("node-a", ws_port).await;
260
261 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-b")));
263 tokio::time::sleep(Duration::from_millis(50)).await;
264
265 let result = send_and_wait(
268 &node,
269 "peer-b",
270 "test-ns",
271 "ping",
272 &serde_json::json!({}),
273 Duration::from_millis(100),
274 |_msg| -> Option<()> { None },
275 )
276 .await;
277
278 assert!(matches!(
279 result,
280 Err(RequestError::Timeout) | Err(RequestError::Send(_))
281 ));
282 }
283
284 #[tokio::test]
285 async fn test_send_and_wait_send_failure_unknown_peer() {
286 let ws_port = random_port().await;
287 let (node, _event_tx) = make_test_node("node-a", ws_port).await;
288
289 let result = send_and_wait(
291 &node,
292 "nonexistent",
293 "test-ns",
294 "ping",
295 &serde_json::json!({}),
296 Duration::from_secs(1),
297 |_msg| -> Option<()> { Some(()) },
298 )
299 .await;
300
301 assert!(matches!(result, Err(RequestError::Send(_))));
302 }
303
304 #[tokio::test]
305 async fn test_send_and_wait_filter_matches() {
306 let msg = NamespacedMessage {
309 from: "peer-b".to_string(),
310 namespace: "test-ns".to_string(),
311 msg_type: "response".to_string(),
312 payload: serde_json::json!({"value": 42}),
313 timestamp: None,
314 };
315
316 let filter = |m: &NamespacedMessage| -> Option<i64> {
318 if m.msg_type == "response" {
319 m.payload.get("value")?.as_i64()
320 } else {
321 None
322 }
323 };
324
325 assert_eq!(filter(&msg), Some(42));
326
327 let non_matching = NamespacedMessage {
329 from: "peer-b".to_string(),
330 namespace: "test-ns".to_string(),
331 msg_type: "other".to_string(),
332 payload: serde_json::json!({}),
333 timestamp: None,
334 };
335 assert_eq!(filter(&non_matching), None);
336 }
337}