1use crate::error::ServiceError;
2use crate::events::ServiceEvent;
3use crate::payloads::{
4 ServiceDiscoveryQueryPayload, ServiceDiscoveryResponsePayload, ServiceRequestPayload,
5 ServiceResponsePayload, DISCOVERY_GOSSIP_MAX_FANOUT, DISCOVERY_INITIAL_BROADCAST_MAX,
6 DISCOVERY_QUERY_DEDUP_TTL_SECS, DISCOVERY_QUERY_DEFAULT_MAX_HOPS,
7 DISCOVERY_QUERY_MAX_DEDUP_ENTRIES, MAX_SERVICE_BODY_SIZE, MAX_SERVICE_METHOD_LEN,
8 MAX_SERVICE_PAYLOAD_SIZE, SVC_DISCOVER_QUERY, SVC_DISCOVER_RESPONSE, SVC_REQUEST, SVC_RESPONSE,
9 VALID_SERVICE_STATUSES,
10};
11use offline_protocol_core::{MessagePriority, ServiceDescriptor};
12use std::collections::{HashMap, VecDeque};
13use std::time::{Duration, Instant};
14use tracing::{debug, info, warn};
15
16#[derive(Debug, Clone)]
18pub struct OutboundMessage {
19 pub recipient: String,
21 pub content: String,
23 pub priority: MessagePriority,
25}
26
27#[must_use]
29pub struct DiscoverResult {
30 pub query_id: String,
32 pub messages: Vec<OutboundMessage>,
34}
35
36#[must_use]
38pub struct SendRequestResult {
39 pub request_id: String,
41 pub message: OutboundMessage,
43}
44
45#[must_use]
47pub struct SendResponseResult {
48 pub message: OutboundMessage,
50}
51
52pub enum ServiceAction {
54 NotHandled,
56 Consumed {
58 messages_to_send: Vec<OutboundMessage>,
60 events_to_emit: Vec<ServiceEvent>,
62 },
63}
64
65pub struct MeshServices {
81 local_services: HashMap<String, ServiceDescriptor>,
82 seen_discovery_queries: HashMap<String, Instant>,
83 seen_discovery_order: VecDeque<String>,
85}
86
87impl MeshServices {
88 pub fn new() -> Self {
90 Self {
91 local_services: HashMap::new(),
92 seen_discovery_queries: HashMap::new(),
93 seen_discovery_order: VecDeque::new(),
94 }
95 }
96
97 pub fn register_service(&mut self, descriptor: ServiceDescriptor) -> Result<(), ServiceError> {
99 let key = descriptor.service_id.as_str().to_string();
100 self.local_services.insert(key, descriptor);
101 Ok(())
102 }
103
104 pub fn unregister_service(&mut self, service_id: &str) -> Result<bool, ServiceError> {
106 Ok(self.local_services.remove(service_id).is_some())
107 }
108
109 pub fn has_service(&self, service_id: &str) -> bool {
111 self.local_services.contains_key(service_id)
112 }
113
114 pub fn discover_services(
120 &mut self,
121 user_id: &str,
122 known_peers: &[String],
123 service_id: Option<&str>,
124 ) -> Result<DiscoverResult, ServiceError> {
125 let query_id = uuid::Uuid::new_v4().to_string();
126
127 let payload = ServiceDiscoveryQueryPayload {
128 query_id: query_id.clone(),
129 originator: user_id.to_string(),
130 service_id: service_id.map(|s| s.to_string()),
131 remaining_hops: DISCOVERY_QUERY_DEFAULT_MAX_HOPS,
132 };
133 let serialized = serde_json::to_string(&payload)
134 .map_err(|e| ServiceError::Serialization(e.to_string()))?;
135 let content = format!("{}{}", SVC_DISCOVER_QUERY, serialized);
136
137 self.record_seen_query(query_id.clone());
138
139 let selected_peers = select_fanout_peers(
141 known_peers,
142 DISCOVERY_INITIAL_BROADCAST_MAX,
143 &query_id,
144 user_id,
145 );
146
147 let messages: Vec<OutboundMessage> = selected_peers
148 .into_iter()
149 .map(|peer| OutboundMessage {
150 recipient: peer,
151 content: content.clone(),
152 priority: MessagePriority::Medium,
153 })
154 .collect();
155
156 info!(query_id = %query_id, service_id = ?service_id, peer_count = messages.len(), "Broadcast service discovery query");
157 Ok(DiscoverResult { query_id, messages })
158 }
159
160 pub fn send_service_request(
162 &self,
163 provider: &str,
164 service_id: &str,
165 method: &str,
166 body: &str,
167 ) -> Result<SendRequestResult, ServiceError> {
168 if body.len() > MAX_SERVICE_BODY_SIZE {
169 return Err(ServiceError::PayloadTooLarge(format!(
170 "request body size {} exceeds maximum {}",
171 body.len(),
172 MAX_SERVICE_BODY_SIZE
173 )));
174 }
175 if method.len() > MAX_SERVICE_METHOD_LEN {
176 return Err(ServiceError::PayloadTooLarge(format!(
177 "method length {} exceeds maximum {}",
178 method.len(),
179 MAX_SERVICE_METHOD_LEN
180 )));
181 }
182
183 let request_id = uuid::Uuid::new_v4().to_string();
184
185 let payload = ServiceRequestPayload {
186 request_id: request_id.clone(),
187 service_id: service_id.to_string(),
188 method: method.to_string(),
189 body: body.to_string(),
190 };
191 let serialized = serde_json::to_string(&payload)
192 .map_err(|e| ServiceError::Serialization(e.to_string()))?;
193 let content = format!("{}{}", SVC_REQUEST, serialized);
194
195 info!(request_id = %request_id, provider = %provider, service_id = %service_id, method = %method, "Sent service request");
196 Ok(SendRequestResult {
197 request_id,
198 message: OutboundMessage {
199 recipient: provider.to_string(),
200 content,
201 priority: MessagePriority::High,
202 },
203 })
204 }
205
206 pub fn respond_to_service_request(
208 &self,
209 request_id: &str,
210 requester: &str,
211 service_id: &str,
212 status: &str,
213 body: &str,
214 ) -> Result<SendResponseResult, ServiceError> {
215 if !VALID_SERVICE_STATUSES.contains(&status) {
216 return Err(ServiceError::InvalidStatus(status.to_string()));
217 }
218 if body.len() > MAX_SERVICE_BODY_SIZE {
219 return Err(ServiceError::PayloadTooLarge(format!(
220 "response body size {} exceeds maximum {}",
221 body.len(),
222 MAX_SERVICE_BODY_SIZE
223 )));
224 }
225
226 let payload = ServiceResponsePayload {
227 request_id: request_id.to_string(),
228 service_id: service_id.to_string(),
229 status: status.to_string(),
230 body: body.to_string(),
231 };
232 let serialized = serde_json::to_string(&payload)
233 .map_err(|e| ServiceError::Serialization(e.to_string()))?;
234 let content = format!("{}{}", SVC_RESPONSE, serialized);
235
236 info!(request_id = %request_id, requester = %requester, status = %status, "Sent service response");
237 Ok(SendResponseResult {
238 message: OutboundMessage {
239 recipient: requester.to_string(),
240 content,
241 priority: MessagePriority::High,
242 },
243 })
244 }
245
246 pub fn handle_incoming_message(
251 &mut self,
252 content: &str,
253 sender: &str,
254 hop_count: u8,
255 user_id: &str,
256 known_peers: &[String],
257 ) -> ServiceAction {
258 if let Some(action) =
259 self.try_handle_discover_query(content, sender, hop_count, user_id, known_peers)
260 {
261 return action;
262 }
263 if let Some(action) = self.try_handle_discover_response(content, sender, user_id) {
264 return action;
265 }
266 if let Some(action) = self.try_handle_request(content, sender) {
267 return action;
268 }
269 if let Some(action) = self.try_handle_response(content, sender) {
270 return action;
271 }
272 ServiceAction::NotHandled
273 }
274
275 pub fn cleanup_expired(&mut self) {
277 let now = Instant::now();
282 let ttl = Duration::from_secs(DISCOVERY_QUERY_DEDUP_TTL_SECS);
283 self.seen_discovery_queries
284 .retain(|_, ts| now.saturating_duration_since(*ts) < ttl);
285 self.seen_discovery_order
286 .retain(|id| self.seen_discovery_queries.contains_key(id));
287 }
288
289 pub fn seen_discovery_queries(&self) -> &HashMap<String, Instant> {
291 &self.seen_discovery_queries
292 }
293
294 fn record_seen_query(&mut self, query_id: String) {
296 if self.seen_discovery_queries.len() >= DISCOVERY_QUERY_MAX_DEDUP_ENTRIES {
297 if let Some(oldest) = self.seen_discovery_order.pop_front() {
299 self.seen_discovery_queries.remove(&oldest);
300 }
301 }
302 self.seen_discovery_queries
303 .insert(query_id.clone(), Instant::now());
304 self.seen_discovery_order.push_back(query_id);
305 }
306
307 fn try_handle_discover_query(
310 &mut self,
311 content: &str,
312 sender: &str,
313 hop_count: u8,
314 user_id: &str,
315 known_peers: &[String],
316 ) -> Option<ServiceAction> {
317 let data = content.strip_prefix(SVC_DISCOVER_QUERY)?;
318
319 if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
320 warn!(sender = %sender, size = data.len(), "Discovery query payload too large, dropping");
321 return Some(ServiceAction::Consumed {
322 messages_to_send: vec![],
323 events_to_emit: vec![],
324 });
325 }
326
327 let payload: ServiceDiscoveryQueryPayload = match serde_json::from_str(data) {
328 Ok(p) => p,
329 Err(_) => {
330 warn!(sender = %sender, "Failed to parse service discovery query payload");
331 return Some(ServiceAction::Consumed {
332 messages_to_send: vec![],
333 events_to_emit: vec![],
334 });
335 }
336 };
337
338 if self.seen_discovery_queries.contains_key(&payload.query_id) {
340 debug!(query_id = %payload.query_id, "Duplicate discovery query, skipping");
341 return Some(ServiceAction::Consumed {
342 messages_to_send: vec![],
343 events_to_emit: vec![],
344 });
345 }
346 self.record_seen_query(payload.query_id.clone());
347
348 let mut messages = Vec::new();
349
350 let matches: Vec<_> = self
352 .local_services
353 .values()
354 .filter(|svc| {
355 payload
356 .service_id
357 .as_ref()
358 .is_none_or(|q| q == svc.service_id.as_str())
359 })
360 .cloned()
361 .collect();
362
363 for svc in &matches {
367 let response = ServiceDiscoveryResponsePayload {
368 query_id: payload.query_id.clone(),
369 service_id: svc.service_id.as_str().to_string(),
370 version: svc.version.clone(),
371 provider_peer_id: user_id.to_string(),
372 capabilities: svc.capabilities.clone(),
373 hop_count,
374 originator: payload.originator.clone(),
375 };
376 if let Ok(serialized) = serde_json::to_string(&response) {
377 let resp_content = format!("{}{}", SVC_DISCOVER_RESPONSE, serialized);
378 messages.push(OutboundMessage {
379 recipient: sender.to_string(),
380 content: resp_content,
381 priority: MessagePriority::Medium,
382 });
383 }
384 }
385
386 let forwarded_count = if payload.remaining_hops > 0 {
388 let mut fwd_payload = payload.clone();
389 fwd_payload.remaining_hops -= 1;
390 let fwd_content = match serde_json::to_string(&fwd_payload) {
391 Ok(serialized) => format!("{}{}", SVC_DISCOVER_QUERY, serialized),
392 Err(e) => {
393 warn!("Failed to re-serialize discovery query for forwarding: {e}");
394 return Some(ServiceAction::Consumed {
395 messages_to_send: messages,
396 events_to_emit: vec![],
397 });
398 }
399 };
400 let eligible_peers: Vec<String> = known_peers
401 .iter()
402 .filter(|p| p.as_str() != sender && p.as_str() != payload.originator)
403 .cloned()
404 .collect();
405 let forward_peers = select_fanout_peers(
406 &eligible_peers,
407 DISCOVERY_GOSSIP_MAX_FANOUT,
408 &payload.query_id,
409 user_id,
410 );
411 for peer in &forward_peers {
412 messages.push(OutboundMessage {
413 recipient: peer.clone(),
414 content: fwd_content.clone(),
415 priority: MessagePriority::Medium,
416 });
417 }
418 forward_peers.len()
419 } else {
420 debug!(query_id = %payload.query_id, "Discovery query reached max hops, not forwarding");
421 0
422 };
423
424 debug!(
425 query_id = %payload.query_id,
426 matches = matches.len(),
427 forwarded_to = forwarded_count,
428 "Processed service discovery query"
429 );
430
431 Some(ServiceAction::Consumed {
432 messages_to_send: messages,
433 events_to_emit: vec![],
434 })
435 }
436
437 fn try_handle_discover_response(
438 &self,
439 content: &str,
440 sender: &str,
441 user_id: &str,
442 ) -> Option<ServiceAction> {
443 let data = content.strip_prefix(SVC_DISCOVER_RESPONSE)?;
444
445 if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
446 warn!(sender = %sender, size = data.len(), "Discovery response payload too large, dropping");
447 return Some(ServiceAction::Consumed {
448 messages_to_send: vec![],
449 events_to_emit: vec![],
450 });
451 }
452
453 let payload: ServiceDiscoveryResponsePayload = match serde_json::from_str(data) {
454 Ok(p) => p,
455 Err(_) => {
456 warn!(sender = %sender, "Failed to parse service discovery response payload");
457 return Some(ServiceAction::Consumed {
458 messages_to_send: vec![],
459 events_to_emit: vec![],
460 });
461 }
462 };
463
464 info!(
465 query_id = %payload.query_id,
466 service_id = %payload.service_id,
467 provider = %payload.provider_peer_id,
468 "Service discovered"
469 );
470
471 if !payload.originator.is_empty() && payload.originator != user_id {
473 debug!(
474 query_id = %payload.query_id,
475 originator = %payload.originator,
476 "Forwarding discovery response toward originator"
477 );
478 let fwd_content = content.to_string();
480 return Some(ServiceAction::Consumed {
481 messages_to_send: vec![OutboundMessage {
482 recipient: payload.originator,
483 content: fwd_content,
484 priority: MessagePriority::Medium,
485 }],
486 events_to_emit: vec![],
487 });
488 }
489
490 Some(ServiceAction::Consumed {
491 messages_to_send: vec![],
492 events_to_emit: vec![ServiceEvent::ServiceDiscovered {
493 query_id: payload.query_id,
494 service_id: payload.service_id,
495 version: payload.version,
496 provider_peer_id: payload.provider_peer_id,
497 capabilities: payload.capabilities,
498 hop_count: payload.hop_count,
499 }],
500 })
501 }
502
503 fn try_handle_request(&self, content: &str, sender: &str) -> Option<ServiceAction> {
504 let data = content.strip_prefix(SVC_REQUEST)?;
505
506 if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
507 warn!(sender = %sender, size = data.len(), "Service request payload too large, dropping");
508 return Some(ServiceAction::Consumed {
509 messages_to_send: vec![],
510 events_to_emit: vec![],
511 });
512 }
513
514 let payload: ServiceRequestPayload = match serde_json::from_str(data) {
515 Ok(p) => p,
516 Err(_) => {
517 warn!(sender = %sender, "Failed to parse service request payload");
518 return Some(ServiceAction::Consumed {
519 messages_to_send: vec![],
520 events_to_emit: vec![],
521 });
522 }
523 };
524
525 if payload.body.len() > MAX_SERVICE_BODY_SIZE {
527 warn!(sender = %sender, size = payload.body.len(), "Service request body too large, dropping");
528 return Some(ServiceAction::Consumed {
529 messages_to_send: vec![],
530 events_to_emit: vec![],
531 });
532 }
533 if payload.method.len() > MAX_SERVICE_METHOD_LEN {
534 warn!(sender = %sender, len = payload.method.len(), "Service request method too long, dropping");
535 return Some(ServiceAction::Consumed {
536 messages_to_send: vec![],
537 events_to_emit: vec![],
538 });
539 }
540
541 if !self.local_services.contains_key(&payload.service_id) {
542 let response = ServiceResponsePayload {
544 request_id: payload.request_id.clone(),
545 service_id: payload.service_id.clone(),
546 status: "not_found".to_string(),
547 body: String::new(),
548 };
549 let mut messages = Vec::new();
550 if let Ok(serialized) = serde_json::to_string(&response) {
551 let resp_content = format!("{}{}", SVC_RESPONSE, serialized);
552 messages.push(OutboundMessage {
553 recipient: sender.to_string(),
554 content: resp_content,
555 priority: MessagePriority::High,
556 });
557 }
558 debug!(
559 request_id = %payload.request_id,
560 service_id = %payload.service_id,
561 "Service not found, auto-responded not_found"
562 );
563 return Some(ServiceAction::Consumed {
564 messages_to_send: messages,
565 events_to_emit: vec![],
566 });
567 }
568
569 info!(
570 request_id = %payload.request_id,
571 service_id = %payload.service_id,
572 method = %payload.method,
573 "Service request received"
574 );
575
576 Some(ServiceAction::Consumed {
577 messages_to_send: vec![],
578 events_to_emit: vec![ServiceEvent::ServiceRequestReceived {
579 request_id: payload.request_id,
580 service_id: payload.service_id,
581 method: payload.method,
582 body: payload.body,
583 sender: sender.to_string(),
584 }],
585 })
586 }
587
588 fn try_handle_response(&self, content: &str, sender: &str) -> Option<ServiceAction> {
589 let data = content.strip_prefix(SVC_RESPONSE)?;
590
591 if data.len() > MAX_SERVICE_PAYLOAD_SIZE {
592 warn!(sender = %sender, size = data.len(), "Service response payload too large, dropping");
593 return Some(ServiceAction::Consumed {
594 messages_to_send: vec![],
595 events_to_emit: vec![],
596 });
597 }
598
599 let payload: ServiceResponsePayload = match serde_json::from_str(data) {
600 Ok(p) => p,
601 Err(_) => {
602 warn!(sender = %sender, "Failed to parse service response payload");
603 return Some(ServiceAction::Consumed {
604 messages_to_send: vec![],
605 events_to_emit: vec![],
606 });
607 }
608 };
609
610 if payload.body.len() > MAX_SERVICE_BODY_SIZE {
612 warn!(sender = %sender, size = payload.body.len(), "Service response body too large, dropping");
613 return Some(ServiceAction::Consumed {
614 messages_to_send: vec![],
615 events_to_emit: vec![],
616 });
617 }
618
619 if !VALID_SERVICE_STATUSES.contains(&payload.status.as_str()) {
621 warn!(sender = %sender, status = %payload.status, "Service response with invalid status, dropping");
622 return Some(ServiceAction::Consumed {
623 messages_to_send: vec![],
624 events_to_emit: vec![],
625 });
626 }
627
628 info!(
629 request_id = %payload.request_id,
630 service_id = %payload.service_id,
631 status = %payload.status,
632 "Service response received"
633 );
634
635 Some(ServiceAction::Consumed {
636 messages_to_send: vec![],
637 events_to_emit: vec![ServiceEvent::ServiceResponseReceived {
638 request_id: payload.request_id,
639 service_id: payload.service_id,
640 status: payload.status,
641 body: payload.body,
642 provider_peer_id: sender.to_string(),
643 }],
644 })
645 }
646}
647
648fn select_fanout_peers(peers: &[String], max: usize, query_id: &str, user_id: &str) -> Vec<String> {
658 if peers.len() <= max {
659 return peers.to_vec();
660 }
661
662 let seed = query_id
663 .bytes()
664 .chain(user_id.bytes())
665 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64))
666 ^ peers.len() as u64;
667
668 let mut indices: Vec<usize> = (0..peers.len()).collect();
669 for i in 0..max {
671 let remaining = peers.len() - i;
672 let j = i + (seed.wrapping_mul((i as u64).wrapping_add(7)) as usize % remaining);
673 indices.swap(i, j);
674 }
675
676 indices[..max]
677 .iter()
678 .map(|&idx| peers[idx].clone())
679 .collect()
680}
681
682impl Default for MeshServices {
683 fn default() -> Self {
684 Self::new()
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691 use offline_protocol_core::ServiceId;
692
693 fn make_descriptor(id: &str) -> ServiceDescriptor {
694 ServiceDescriptor {
695 service_id: ServiceId::new(id).unwrap(),
696 version: "1.0".to_string(),
697 capabilities: HashMap::new(),
698 }
699 }
700
701 #[test]
702 fn test_register_and_unregister() {
703 let mut svc = MeshServices::new();
704 svc.register_service(make_descriptor("echo.v1")).unwrap();
705 assert!(svc.has_service("echo.v1"));
706
707 assert!(svc.unregister_service("echo.v1").unwrap());
708 assert!(!svc.has_service("echo.v1"));
709
710 assert!(!svc.unregister_service("echo.v1").unwrap());
711 }
712
713 #[test]
714 fn test_discover_services_generates_messages() {
715 let mut svc = MeshServices::new();
716 let peers = vec!["alice".to_string(), "bob".to_string()];
717 let result = svc.discover_services("user1", &peers, None).unwrap();
718 assert!(!result.query_id.is_empty());
719 assert_eq!(result.messages.len(), 2);
720 assert!(svc.seen_discovery_queries().contains_key(&result.query_id));
721 }
722
723 #[test]
724 fn test_discover_services_no_peers() {
725 let mut svc = MeshServices::new();
726 let result = svc
727 .discover_services("user1", &[], Some("weather"))
728 .unwrap();
729 assert!(result.messages.is_empty());
730 }
731
732 #[test]
733 fn test_discover_services_initial_broadcast_fanout_limit() {
734 let mut svc = MeshServices::new();
735 let peers: Vec<String> = (0..50).map(|i| format!("peer-{i}")).collect();
736 let result = svc.discover_services("user1", &peers, None).unwrap();
737 assert_eq!(result.messages.len(), DISCOVERY_INITIAL_BROADCAST_MAX);
738 }
739
740 #[test]
741 fn test_send_service_request() {
742 let svc = MeshServices::new();
743 let result = svc
744 .send_service_request("bob", "echo.v1", "ping", "{}")
745 .unwrap();
746 assert!(!result.request_id.is_empty());
747 assert_eq!(result.message.recipient, "bob");
748 assert!(result.message.content.starts_with(SVC_REQUEST));
749 }
750
751 #[test]
752 fn test_send_service_request_body_too_large() {
753 let svc = MeshServices::new();
754 let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
755 let result = svc.send_service_request("bob", "echo.v1", "ping", &large_body);
756 assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
757 }
758
759 #[test]
760 fn test_send_service_request_method_too_long() {
761 let svc = MeshServices::new();
762 let long_method = "m".repeat(MAX_SERVICE_METHOD_LEN + 1);
763 let result = svc.send_service_request("bob", "echo.v1", &long_method, "{}");
764 assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
765 }
766
767 #[test]
768 fn test_respond_to_service_request() {
769 let svc = MeshServices::new();
770 let result = svc
771 .respond_to_service_request("req-1", "alice", "echo.v1", "ok", "pong")
772 .unwrap();
773 assert_eq!(result.message.recipient, "alice");
774 assert!(result.message.content.starts_with(SVC_RESPONSE));
775 }
776
777 #[test]
778 fn test_respond_to_service_request_invalid_status() {
779 let svc = MeshServices::new();
780 let result = svc.respond_to_service_request("req-1", "alice", "echo.v1", "invalid", "");
781 assert!(matches!(result, Err(ServiceError::InvalidStatus(_))));
782 }
783
784 #[test]
785 fn test_respond_to_service_request_body_too_large() {
786 let svc = MeshServices::new();
787 let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
788 let result = svc.respond_to_service_request("req-1", "alice", "echo.v1", "ok", &large_body);
789 assert!(matches!(result, Err(ServiceError::PayloadTooLarge(_))));
790 }
791
792 #[test]
793 fn test_respond_valid_statuses() {
794 let svc = MeshServices::new();
795 for status in VALID_SERVICE_STATUSES {
796 let result =
797 svc.respond_to_service_request("req-1", "alice", "echo.v1", status, "body");
798 assert!(result.is_ok(), "status '{status}' should be valid");
799 }
800 }
801
802 #[test]
803 fn test_handle_discover_query_with_match() {
804 let mut svc = MeshServices::new();
805 svc.register_service(ServiceDescriptor {
806 service_id: ServiceId::new("weather").unwrap(),
807 version: "2.0".to_string(),
808 capabilities: {
809 let mut m = HashMap::new();
810 m.insert("format".to_string(), "json".to_string());
811 m
812 },
813 })
814 .unwrap();
815
816 let payload = ServiceDiscoveryQueryPayload {
817 query_id: "q-001".to_string(),
818 originator: "alice".to_string(),
819 service_id: Some("weather".to_string()),
820 remaining_hops: 5,
821 };
822 let content = format!(
823 "{}{}",
824 SVC_DISCOVER_QUERY,
825 serde_json::to_string(&payload).unwrap()
826 );
827
828 let peers = vec!["charlie".to_string()];
829 let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
830 match action {
831 ServiceAction::Consumed {
832 messages_to_send,
833 events_to_emit,
834 } => {
835 assert!(!messages_to_send.is_empty());
836 assert!(events_to_emit.is_empty());
837 assert!(messages_to_send.iter().any(
839 |m| m.recipient == "alice" && m.content.starts_with(SVC_DISCOVER_RESPONSE)
840 ));
841 }
842 ServiceAction::NotHandled => panic!("Should have been handled"),
843 }
844 assert!(svc.seen_discovery_queries().contains_key("q-001"));
845 }
846
847 #[test]
848 fn test_handle_discover_query_dedup() {
849 let mut svc = MeshServices::new();
850
851 let payload = ServiceDiscoveryQueryPayload {
852 query_id: "q-dedup".to_string(),
853 originator: "alice".to_string(),
854 service_id: None,
855 remaining_hops: 5,
856 };
857 let content = format!(
858 "{}{}",
859 SVC_DISCOVER_QUERY,
860 serde_json::to_string(&payload).unwrap()
861 );
862
863 let a1 = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
865 assert!(matches!(a1, ServiceAction::Consumed { .. }));
866
867 let a2 = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
869 assert!(matches!(a2, ServiceAction::Consumed { .. }));
870 }
871
872 #[test]
873 fn test_handle_discover_response() {
874 let mut svc = MeshServices::new();
875
876 let payload = ServiceDiscoveryResponsePayload {
877 query_id: "q-123".to_string(),
878 service_id: "weather".to_string(),
879 version: "2.0".to_string(),
880 provider_peer_id: "bob".to_string(),
881 capabilities: HashMap::new(),
882 hop_count: 1,
883 originator: "user1".to_string(),
884 };
885 let content = format!(
886 "{}{}",
887 SVC_DISCOVER_RESPONSE,
888 serde_json::to_string(&payload).unwrap()
889 );
890
891 let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
892 match action {
893 ServiceAction::Consumed { events_to_emit, .. } => {
894 assert_eq!(events_to_emit.len(), 1);
895 match &events_to_emit[0] {
896 ServiceEvent::ServiceDiscovered {
897 query_id,
898 service_id,
899 ..
900 } => {
901 assert_eq!(query_id, "q-123");
902 assert_eq!(service_id, "weather");
903 }
904 other => panic!("Wrong event: {:?}", other),
905 }
906 }
907 ServiceAction::NotHandled => panic!("Should have been handled"),
908 }
909 }
910
911 #[test]
912 fn test_handle_request_unregistered() {
913 let mut svc = MeshServices::new();
914
915 let payload = ServiceRequestPayload {
916 request_id: "req-001".to_string(),
917 service_id: "nonexistent".to_string(),
918 method: "get".to_string(),
919 body: "{}".to_string(),
920 };
921 let content = format!(
922 "{}{}",
923 SVC_REQUEST,
924 serde_json::to_string(&payload).unwrap()
925 );
926
927 let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
928 match action {
929 ServiceAction::Consumed {
930 messages_to_send,
931 events_to_emit,
932 } => {
933 assert_eq!(messages_to_send.len(), 1);
934 assert!(messages_to_send[0].content.contains("not_found"));
935 assert!(events_to_emit.is_empty());
936 }
937 ServiceAction::NotHandled => panic!("Should have been handled"),
938 }
939 }
940
941 #[test]
942 fn test_handle_request_registered() {
943 let mut svc = MeshServices::new();
944 svc.register_service(make_descriptor("echo")).unwrap();
945
946 let payload = ServiceRequestPayload {
947 request_id: "req-002".to_string(),
948 service_id: "echo".to_string(),
949 method: "ping".to_string(),
950 body: "hello".to_string(),
951 };
952 let content = format!(
953 "{}{}",
954 SVC_REQUEST,
955 serde_json::to_string(&payload).unwrap()
956 );
957
958 let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
959 match action {
960 ServiceAction::Consumed {
961 messages_to_send,
962 events_to_emit,
963 } => {
964 assert!(messages_to_send.is_empty());
965 assert_eq!(events_to_emit.len(), 1);
966 match &events_to_emit[0] {
967 ServiceEvent::ServiceRequestReceived {
968 request_id,
969 service_id,
970 method,
971 ..
972 } => {
973 assert_eq!(request_id, "req-002");
974 assert_eq!(service_id, "echo");
975 assert_eq!(method, "ping");
976 }
977 other => panic!("Wrong event: {:?}", other),
978 }
979 }
980 ServiceAction::NotHandled => panic!("Should have been handled"),
981 }
982 }
983
984 #[test]
985 fn test_handle_response() {
986 let mut svc = MeshServices::new();
987
988 let payload = ServiceResponsePayload {
989 request_id: "req-003".to_string(),
990 service_id: "echo".to_string(),
991 status: "ok".to_string(),
992 body: "pong".to_string(),
993 };
994 let content = format!(
995 "{}{}",
996 SVC_RESPONSE,
997 serde_json::to_string(&payload).unwrap()
998 );
999
1000 let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
1001 match action {
1002 ServiceAction::Consumed { events_to_emit, .. } => {
1003 assert_eq!(events_to_emit.len(), 1);
1004 match &events_to_emit[0] {
1005 ServiceEvent::ServiceResponseReceived {
1006 request_id, status, ..
1007 } => {
1008 assert_eq!(request_id, "req-003");
1009 assert_eq!(status, "ok");
1010 }
1011 other => panic!("Wrong event: {:?}", other),
1012 }
1013 }
1014 ServiceAction::NotHandled => panic!("Should have been handled"),
1015 }
1016 }
1017
1018 #[test]
1019 fn test_not_handled() {
1020 let mut svc = MeshServices::new();
1021 let action = svc.handle_incoming_message("Hello, world!", "alice", 0, "user1", &[]);
1022 assert!(matches!(action, ServiceAction::NotHandled));
1023 }
1024
1025 #[test]
1026 fn test_cleanup_expired() {
1027 let mut svc = MeshServices::new();
1028 svc.seen_discovery_queries.insert(
1029 "old-query".to_string(),
1030 Instant::now() - Duration::from_secs(120),
1031 );
1032 svc.seen_discovery_order.push_back("old-query".to_string());
1033 svc.seen_discovery_queries
1034 .insert("fresh-query".to_string(), Instant::now());
1035 svc.seen_discovery_order
1036 .push_back("fresh-query".to_string());
1037
1038 svc.cleanup_expired();
1039
1040 assert!(!svc.seen_discovery_queries().contains_key("old-query"));
1041 assert!(svc.seen_discovery_queries().contains_key("fresh-query"));
1042 }
1043
1044 #[test]
1045 fn test_gossip_fanout_limit() {
1046 let mut svc = MeshServices::new();
1047
1048 let payload = ServiceDiscoveryQueryPayload {
1049 query_id: "q-fanout".to_string(),
1050 originator: "alice".to_string(),
1051 service_id: None,
1052 remaining_hops: 5,
1053 };
1054 let content = format!(
1055 "{}{}",
1056 SVC_DISCOVER_QUERY,
1057 serde_json::to_string(&payload).unwrap()
1058 );
1059
1060 let peers: Vec<String> = (0..20).map(|i| format!("peer-{i}")).collect();
1062 let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1063 match action {
1064 ServiceAction::Consumed {
1065 messages_to_send, ..
1066 } => {
1067 assert_eq!(messages_to_send.len(), DISCOVERY_GOSSIP_MAX_FANOUT);
1069 }
1070 ServiceAction::NotHandled => panic!("Should have been handled"),
1071 }
1072 }
1073
1074 #[test]
1075 fn test_dedup_cap_evicts_oldest() {
1076 let mut svc = MeshServices::new();
1077
1078 let base = Instant::now() - Duration::from_secs(30);
1080 for i in 0..DISCOVERY_QUERY_MAX_DEDUP_ENTRIES {
1081 let key = format!("q-{i}");
1082 svc.seen_discovery_queries
1083 .insert(key.clone(), base + Duration::from_millis(i as u64));
1084 svc.seen_discovery_order.push_back(key);
1085 }
1086 assert_eq!(
1087 svc.seen_discovery_queries().len(),
1088 DISCOVERY_QUERY_MAX_DEDUP_ENTRIES
1089 );
1090
1091 svc.record_seen_query("q-new".to_string());
1093 assert_eq!(
1094 svc.seen_discovery_queries().len(),
1095 DISCOVERY_QUERY_MAX_DEDUP_ENTRIES
1096 );
1097 assert!(!svc.seen_discovery_queries().contains_key("q-0"));
1098 assert!(svc.seen_discovery_queries().contains_key("q-new"));
1099 }
1100
1101 #[test]
1102 fn test_remaining_hops_zero_no_forwarding() {
1103 let mut svc = MeshServices::new();
1104
1105 let payload = ServiceDiscoveryQueryPayload {
1106 query_id: "q-nohop".to_string(),
1107 originator: "alice".to_string(),
1108 service_id: None,
1109 remaining_hops: 0,
1110 };
1111 let content = format!(
1112 "{}{}",
1113 SVC_DISCOVER_QUERY,
1114 serde_json::to_string(&payload).unwrap()
1115 );
1116
1117 let peers = vec!["bob".to_string(), "charlie".to_string()];
1118 let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1119 match action {
1120 ServiceAction::Consumed {
1121 messages_to_send, ..
1122 } => {
1123 assert!(
1125 messages_to_send.is_empty(),
1126 "Should not forward when remaining_hops=0"
1127 );
1128 }
1129 ServiceAction::NotHandled => panic!("Should have been handled"),
1130 }
1131 }
1132
1133 #[test]
1134 fn test_oversized_payload_dropped() {
1135 let mut svc = MeshServices::new();
1136
1137 let huge_body = "x".repeat(MAX_SERVICE_PAYLOAD_SIZE + 1);
1139 let content = format!("{}{}", SVC_REQUEST, huge_body);
1140
1141 let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1142 assert!(matches!(
1143 action,
1144 ServiceAction::Consumed {
1145 messages_to_send,
1146 events_to_emit,
1147 } if messages_to_send.is_empty() && events_to_emit.is_empty()
1148 ));
1149 }
1150
1151 #[test]
1152 fn test_oversized_request_body_dropped() {
1153 let mut svc = MeshServices::new();
1154 svc.register_service(make_descriptor("echo")).unwrap();
1155
1156 let large_body = "x".repeat(MAX_SERVICE_BODY_SIZE + 1);
1157 let payload = ServiceRequestPayload {
1158 request_id: "req-big".to_string(),
1159 service_id: "echo".to_string(),
1160 method: "ping".to_string(),
1161 body: large_body,
1162 };
1163 let content = format!(
1164 "{}{}",
1165 SVC_REQUEST,
1166 serde_json::to_string(&payload).unwrap()
1167 );
1168
1169 let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1170 match action {
1171 ServiceAction::Consumed {
1172 messages_to_send,
1173 events_to_emit,
1174 } => {
1175 assert!(messages_to_send.is_empty());
1176 assert!(
1177 events_to_emit.is_empty(),
1178 "Should not emit event for oversized body"
1179 );
1180 }
1181 ServiceAction::NotHandled => panic!("Should have been handled"),
1182 }
1183 }
1184
1185 #[test]
1186 fn test_oversized_method_dropped() {
1187 let mut svc = MeshServices::new();
1188 svc.register_service(make_descriptor("echo")).unwrap();
1189
1190 let long_method = "m".repeat(MAX_SERVICE_METHOD_LEN + 1);
1191 let payload = ServiceRequestPayload {
1192 request_id: "req-longmethod".to_string(),
1193 service_id: "echo".to_string(),
1194 method: long_method,
1195 body: "{}".to_string(),
1196 };
1197 let content = format!(
1198 "{}{}",
1199 SVC_REQUEST,
1200 serde_json::to_string(&payload).unwrap()
1201 );
1202
1203 let action = svc.handle_incoming_message(&content, "alice", 0, "user1", &[]);
1204 match action {
1205 ServiceAction::Consumed {
1206 messages_to_send,
1207 events_to_emit,
1208 } => {
1209 assert!(messages_to_send.is_empty());
1210 assert!(
1211 events_to_emit.is_empty(),
1212 "Should not emit event for oversized method"
1213 );
1214 }
1215 ServiceAction::NotHandled => panic!("Should have been handled"),
1216 }
1217 }
1218
1219 #[test]
1220 fn test_handle_response_invalid_status_dropped() {
1221 let mut svc = MeshServices::new();
1222
1223 let payload = ServiceResponsePayload {
1224 request_id: "req-bad".to_string(),
1225 service_id: "echo".to_string(),
1226 status: "invalid_status".to_string(),
1227 body: "data".to_string(),
1228 };
1229 let content = format!(
1230 "{}{}",
1231 SVC_RESPONSE,
1232 serde_json::to_string(&payload).unwrap()
1233 );
1234
1235 let action = svc.handle_incoming_message(&content, "bob", 0, "user1", &[]);
1236 match action {
1237 ServiceAction::Consumed {
1238 messages_to_send,
1239 events_to_emit,
1240 } => {
1241 assert!(messages_to_send.is_empty());
1242 assert!(
1243 events_to_emit.is_empty(),
1244 "Should not emit event for invalid status"
1245 );
1246 }
1247 ServiceAction::NotHandled => panic!("Should have been handled"),
1248 }
1249 }
1250
1251 #[test]
1252 fn test_handle_discover_query_with_match_and_forwarding() {
1253 let mut svc = MeshServices::new();
1254 svc.register_service(ServiceDescriptor {
1255 service_id: ServiceId::new("weather").unwrap(),
1256 version: "2.0".to_string(),
1257 capabilities: HashMap::new(),
1258 })
1259 .unwrap();
1260
1261 let payload = ServiceDiscoveryQueryPayload {
1262 query_id: "q-combined".to_string(),
1263 originator: "alice".to_string(),
1264 service_id: Some("weather".to_string()),
1265 remaining_hops: 5,
1266 };
1267 let content = format!(
1268 "{}{}",
1269 SVC_DISCOVER_QUERY,
1270 serde_json::to_string(&payload).unwrap()
1271 );
1272
1273 let peers = vec!["bob".to_string(), "charlie".to_string(), "dave".to_string()];
1275 let action = svc.handle_incoming_message(&content, "alice", 1, "user1", &peers);
1276 match action {
1277 ServiceAction::Consumed {
1278 messages_to_send,
1279 events_to_emit,
1280 } => {
1281 let responses: Vec<_> = messages_to_send
1283 .iter()
1284 .filter(|m| m.content.starts_with(SVC_DISCOVER_RESPONSE))
1285 .collect();
1286 assert_eq!(responses.len(), 1, "Should have one discovery response");
1287 assert_eq!(
1288 responses[0].recipient, "alice",
1289 "Response should go to sender"
1290 );
1291
1292 let forwards: Vec<_> = messages_to_send
1294 .iter()
1295 .filter(|m| m.content.starts_with(SVC_DISCOVER_QUERY))
1296 .collect();
1297 assert_eq!(forwards.len(), 3, "Should forward to all 3 eligible peers");
1298 assert!(
1300 forwards.iter().all(|m| m.recipient != "alice"),
1301 "Should not forward back to sender"
1302 );
1303
1304 assert!(events_to_emit.is_empty());
1306 }
1307 ServiceAction::NotHandled => panic!("Should have been handled"),
1308 }
1309 }
1310
1311 #[test]
1312 fn test_select_fanout_peers_all_when_under_limit() {
1313 let peers: Vec<String> = vec!["a".into(), "b".into(), "c".into()];
1314 let selected = select_fanout_peers(&peers, 5, "q-1", "user1");
1315 assert_eq!(selected.len(), 3);
1316 }
1317
1318 #[test]
1319 fn test_select_fanout_peers_caps_at_max() {
1320 let peers: Vec<String> = (0..50).map(|i| format!("p-{i}")).collect();
1321 let selected = select_fanout_peers(&peers, 5, "q-1", "user1");
1322 assert_eq!(selected.len(), 5);
1323 }
1324
1325 #[test]
1326 fn test_select_fanout_peers_deterministic() {
1327 let peers: Vec<String> = (0..20).map(|i| format!("p-{i}")).collect();
1328 let a = select_fanout_peers(&peers, 5, "q-1", "user1");
1329 let b = select_fanout_peers(&peers, 5, "q-1", "user1");
1330 assert_eq!(a, b, "Same inputs should produce same selection");
1331 }
1332
1333 #[test]
1334 fn test_select_fanout_peers_varies_by_user() {
1335 let peers: Vec<String> = (0..20).map(|i| format!("p-{i}")).collect();
1336 let a = select_fanout_peers(&peers, 5, "q-1", "user1");
1337 let b = select_fanout_peers(&peers, 5, "q-1", "user2");
1338 assert_ne!(
1341 a, b,
1342 "Different users should typically select different peers"
1343 );
1344 }
1345
1346 #[test]
1347 fn test_discovery_response_forwarded_to_originator() {
1348 let mut svc = MeshServices::new();
1349
1350 let payload = ServiceDiscoveryResponsePayload {
1352 query_id: "q-200".to_string(),
1353 service_id: "svc1".to_string(),
1354 version: "1.0".to_string(),
1355 provider_peer_id: "provider1".to_string(),
1356 capabilities: HashMap::new(),
1357 hop_count: 2,
1358 originator: "original_requester".to_string(),
1359 };
1360 let content = format!(
1361 "{}{}",
1362 SVC_DISCOVER_RESPONSE,
1363 serde_json::to_string(&payload).unwrap()
1364 );
1365
1366 let action = svc.handle_incoming_message(&content, "provider1", 0, "relay_node", &[]);
1368 match action {
1369 ServiceAction::Consumed {
1370 messages_to_send,
1371 events_to_emit,
1372 } => {
1373 assert!(
1375 events_to_emit.is_empty(),
1376 "Relay node should not emit ServiceDiscovered"
1377 );
1378 assert_eq!(messages_to_send.len(), 1);
1379 assert_eq!(messages_to_send[0].recipient, "original_requester");
1380 }
1381 ServiceAction::NotHandled => panic!("Should have been handled"),
1382 }
1383 }
1384
1385 #[test]
1386 fn test_discovery_response_consumed_by_originator() {
1387 let mut svc = MeshServices::new();
1388
1389 let payload = ServiceDiscoveryResponsePayload {
1391 query_id: "q-300".to_string(),
1392 service_id: "svc2".to_string(),
1393 version: "1.0".to_string(),
1394 provider_peer_id: "provider2".to_string(),
1395 capabilities: HashMap::new(),
1396 hop_count: 1,
1397 originator: "me".to_string(),
1398 };
1399 let content = format!(
1400 "{}{}",
1401 SVC_DISCOVER_RESPONSE,
1402 serde_json::to_string(&payload).unwrap()
1403 );
1404
1405 let action = svc.handle_incoming_message(&content, "relay_node", 0, "me", &[]);
1406 match action {
1407 ServiceAction::Consumed {
1408 messages_to_send,
1409 events_to_emit,
1410 } => {
1411 assert!(messages_to_send.is_empty(), "Originator should not forward");
1412 assert_eq!(events_to_emit.len(), 1);
1413 match &events_to_emit[0] {
1414 ServiceEvent::ServiceDiscovered { service_id, .. } => {
1415 assert_eq!(service_id, "svc2");
1416 }
1417 other => panic!("Wrong event: {:?}", other),
1418 }
1419 }
1420 ServiceAction::NotHandled => panic!("Should have been handled"),
1421 }
1422 }
1423}