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 id: id.to_string(),
133 hostname: format!("truffle-test-{id}"),
134 name: format!("Test Node {id}"),
135 dns_name: None,
136 ip: Some("127.0.0.1".parse().unwrap()),
137 },
138 local_addr: PeerAddr {
139 ip: Some("127.0.0.1".parse().unwrap()),
140 hostname: format!("truffle-test-{id}"),
141 dns_name: None,
142 },
143 peer_event_tx,
144 mock_peers: Arc::new(tokio::sync::RwLock::new(Vec::new())),
145 }
146 }
147
148 fn event_sender(&self) -> tokio::sync::broadcast::Sender<NetworkPeerEvent> {
149 self.peer_event_tx.clone()
150 }
151 }
152
153 impl NetworkProvider for MockNetworkProvider {
154 fn local_identity(&self) -> NodeIdentity { self.identity.clone() }
155 fn local_addr(&self) -> PeerAddr { self.local_addr.clone() }
156 fn peer_events(&self) -> tokio::sync::broadcast::Receiver<NetworkPeerEvent> {
157 self.peer_event_tx.subscribe()
158 }
159
160 async fn start(&mut self) -> Result<(), NetworkError> { Ok(()) }
161 async fn stop(&mut self) -> Result<(), NetworkError> { Ok(()) }
162 async fn peers(&self) -> Vec<NetworkPeer> { self.mock_peers.read().await.clone() }
163 async fn dial_tcp(&self, _addr: &str, _port: u16) -> Result<tokio::net::TcpStream, NetworkError> {
164 Err(NetworkError::DialFailed("mock".into()))
165 }
166 async fn listen_tcp(&self, _port: u16) -> Result<NetworkTcpListener, NetworkError> {
167 Err(NetworkError::ListenFailed("mock".into()))
168 }
169 async fn unlisten_tcp(&self, _port: u16) -> Result<(), NetworkError> { Ok(()) }
170 async fn bind_udp(&self, _port: u16) -> Result<NetworkUdpSocket, NetworkError> {
171 Err(NetworkError::NotRunning)
172 }
173 async fn ping(&self, _addr: &str) -> Result<PingResult, NetworkError> {
174 Ok(PingResult { latency: Duration::from_millis(1), connection: "direct".to_string(), peer_addr: None })
175 }
176 async fn health(&self) -> HealthInfo {
177 HealthInfo { state: "running".to_string(), healthy: true, ..Default::default() }
178 }
179 }
180
181 fn ws_config(port: u16) -> WsConfig {
182 WsConfig {
183 port,
184 ping_interval: Duration::from_secs(300),
185 pong_timeout: Duration::from_secs(300),
186 ..Default::default()
187 }
188 }
189
190 async fn random_port() -> u16 {
191 let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
192 l.local_addr().unwrap().port()
193 }
194
195 fn make_loopback_peer(id: &str) -> NetworkPeer {
196 NetworkPeer {
197 id: id.to_string(),
198 hostname: format!("truffle-test-{id}"),
199 ip: "127.0.0.1".parse().unwrap(),
200 online: true,
201 cur_addr: Some("127.0.0.1:41641".to_string()),
202 relay: None,
203 os: Some("linux".to_string()),
204 last_seen: Some("2026-03-25T12:00:00Z".to_string()),
205 key_expiry: None,
206 dns_name: None,
207 }
208 }
209
210 async fn make_test_node(
211 id: &str,
212 ws_port: u16,
213 ) -> (
214 Node<MockNetworkProvider>,
215 tokio::sync::broadcast::Sender<NetworkPeerEvent>,
216 ) {
217 let provider = MockNetworkProvider::new(id);
218 let event_tx = provider.event_sender();
219 let network = Arc::new(provider);
220 let ws_transport = Arc::new(WebSocketTransport::new(network.clone(), ws_config(ws_port)));
221 let session = Arc::new(PeerRegistry::new(network.clone(), ws_transport));
222 session.start().await;
223
224 let codec: Arc<dyn EnvelopeCodec> = Arc::new(JsonCodec);
225 let node = Node::from_parts(network, session, codec);
226 (node, event_tx)
227 }
228
229 #[tokio::test]
230 async fn test_send_and_wait_timeout() {
231 let ws_port = random_port().await;
232 let (node, event_tx) = make_test_node("node-a", ws_port).await;
233
234 let _ = event_tx.send(NetworkPeerEvent::Joined(make_loopback_peer("peer-b")));
236 tokio::time::sleep(Duration::from_millis(50)).await;
237
238 let result = send_and_wait(
241 &node,
242 "peer-b",
243 "test-ns",
244 "ping",
245 &serde_json::json!({}),
246 Duration::from_millis(100),
247 |_msg| -> Option<()> { None },
248 )
249 .await;
250
251 assert!(matches!(result, Err(RequestError::Timeout) | Err(RequestError::Send(_))));
252 }
253
254 #[tokio::test]
255 async fn test_send_and_wait_send_failure_unknown_peer() {
256 let ws_port = random_port().await;
257 let (node, _event_tx) = make_test_node("node-a", ws_port).await;
258
259 let result = send_and_wait(
261 &node,
262 "nonexistent",
263 "test-ns",
264 "ping",
265 &serde_json::json!({}),
266 Duration::from_secs(1),
267 |_msg| -> Option<()> { Some(()) },
268 )
269 .await;
270
271 assert!(matches!(result, Err(RequestError::Send(_))));
272 }
273
274 #[tokio::test]
275 async fn test_send_and_wait_filter_matches() {
276 let msg = NamespacedMessage {
279 from: "peer-b".to_string(),
280 namespace: "test-ns".to_string(),
281 msg_type: "response".to_string(),
282 payload: serde_json::json!({"value": 42}),
283 timestamp: None,
284 };
285
286 let filter = |m: &NamespacedMessage| -> Option<i64> {
288 if m.msg_type == "response" {
289 m.payload.get("value")?.as_i64()
290 } else {
291 None
292 }
293 };
294
295 assert_eq!(filter(&msg), Some(42));
296
297 let non_matching = NamespacedMessage {
299 from: "peer-b".to_string(),
300 namespace: "test-ns".to_string(),
301 msg_type: "other".to_string(),
302 payload: serde_json::json!({}),
303 timestamp: None,
304 };
305 assert_eq!(filter(&non_matching), None);
306 }
307}