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