1use std::collections::{HashMap, HashSet};
15use std::sync::Mutex;
16
17use serde::{Deserialize, Serialize};
18
19#[allow(dead_code)]
22pub const EVENT_BLOCK_CLOSE: &str = "blockclose";
23#[allow(dead_code)]
24pub const EVENT_CONN_CHANGE: &str = "connchange";
25pub const EVENT_SYS_INFO: &str = "sysinfo";
26pub const EVENT_CONTROLLER_STATUS: &str = "controllerstatus";
27pub const EVENT_WAVE_OBJ_UPDATE: &str = "waveobj:update";
28pub const EVENT_BLOCK_FILE: &str = "blockfile";
29pub const EVENT_INSTALL_PROGRESS: &str = "install_progress";
30#[allow(dead_code)]
31pub const EVENT_CONFIG: &str = "config";
32#[allow(dead_code)]
33pub const EVENT_USER_INPUT: &str = "userinput";
34pub const EVENT_AGENT_MESSAGE_ACCEPTED: &str = "agent-message-accepted";
39#[allow(dead_code)]
40pub const EVENT_ROUTE_GONE: &str = "route:gone";
41pub const EVENT_BLOCK_STATS: &str = "blockstats";
42pub const EVENT_AGENT_HEALTH: &str = "agenthealth";
43pub const EVENT_AGENT_FAILURE: &str = "agentfailure";
47pub const EVENT_SHELL_NODE_CREATE: &str = "shell_node_create";
51pub const EVENT_SHELL_CHUNK: &str = "shell_chunk";
55pub const EVENT_BLOCK_ACTIVITY: &str = "block:activity";
60
61#[allow(dead_code)]
63pub const FILE_OP_CREATE: &str = "create";
64#[allow(dead_code)]
65pub const FILE_OP_DELETE: &str = "delete";
66pub const FILE_OP_APPEND: &str = "append";
67pub const FILE_OP_TRUNCATE: &str = "truncate";
68#[allow(dead_code)]
69pub const FILE_OP_INVALIDATE: &str = "invalidate";
70
71const MAX_PERSIST: usize = 4096;
72const REMAKE_ARR_THRESHOLD: usize = 10 * 1024;
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct WaveEvent {
78 pub event: String,
79 #[serde(skip_serializing_if = "Vec::is_empty", default)]
80 pub scopes: Vec<String>,
81 #[serde(skip_serializing_if = "String::is_empty", default)]
82 pub sender: String,
83 #[serde(skip_serializing_if = "is_zero", default)]
84 pub persist: usize,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub data: Option<serde_json::Value>,
87}
88
89fn is_zero(v: &usize) -> bool {
90 *v == 0
91}
92
93impl WaveEvent {
94 #[allow(dead_code)]
95 pub fn has_scope(&self, scope: &str) -> bool {
96 self.scopes.iter().any(|s| s == scope)
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct SubscriptionRequest {
102 pub event: String,
103 #[serde(skip_serializing_if = "Vec::is_empty", default)]
104 pub scopes: Vec<String>,
105 #[serde(default)]
106 pub allscopes: bool,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct WSFileEventData {
111 pub zoneid: String,
112 pub filename: String,
113 pub fileop: String,
114 #[serde(skip_serializing_if = "String::is_empty", default)]
115 pub data64: String,
116}
117
118pub trait WpsClient: Send + Sync {
122 fn send_event(&self, route_id: &str, event: WaveEvent);
123}
124
125#[derive(Default)]
128struct BrokerSubscription {
129 all_subs: Vec<String>,
131 scope_subs: HashMap<String, Vec<String>>,
133 star_subs: HashMap<String, Vec<String>>,
135}
136
137impl BrokerSubscription {
138 fn is_empty(&self) -> bool {
139 self.all_subs.is_empty() && self.scope_subs.is_empty() && self.star_subs.is_empty()
140 }
141}
142
143#[derive(Hash, Eq, PartialEq, Clone)]
144struct PersistKey {
145 event: String,
146 scope: String,
147}
148
149struct PersistEventWrap {
150 arr_total_adds: usize,
151 events: Vec<WaveEvent>,
152}
153
154pub struct Broker {
158 inner: Mutex<BrokerInner>,
159}
160
161type ReplayKey = (String, String, String);
168
169struct BrokerInner {
170 client: Option<Box<dyn WpsClient>>,
171 sub_map: HashMap<String, BrokerSubscription>,
172 persist_map: HashMap<PersistKey, PersistEventWrap>,
173 replayed: HashSet<ReplayKey>,
174}
175
176impl Broker {
177 pub fn new() -> Self {
178 Self {
179 inner: Mutex::new(BrokerInner {
180 client: None,
181 sub_map: HashMap::new(),
182 persist_map: HashMap::new(),
183 replayed: HashSet::new(),
184 }),
185 }
186 }
187
188 pub fn set_client(&self, client: Box<dyn WpsClient>) {
189 let mut inner = self.inner.lock().unwrap();
190 inner.client = Some(client);
191 }
192
193 pub fn subscribe(&self, route_id: &str, sub: SubscriptionRequest) {
202 if sub.event.is_empty() {
203 return;
204 }
205 let mut inner = self.inner.lock().unwrap();
206 Self::unsubscribe_nolock(&mut inner, route_id, &sub.event);
208
209 let bs = inner
210 .sub_map
211 .entry(sub.event.clone())
212 .or_default();
213
214 if sub.allscopes {
215 add_unique(&mut bs.all_subs, route_id);
216 } else {
217 for scope in &sub.scopes {
218 if scope_has_star(scope) {
219 add_to_scope_map(&mut bs.star_subs, scope, route_id);
220 } else {
221 add_to_scope_map(&mut bs.scope_subs, scope, route_id);
222 }
223 }
224 }
225
226 Self::replay_to_route(&mut inner, route_id, &sub);
227 }
228
229 fn replay_to_route(
248 inner: &mut BrokerInner,
249 route_id: &str,
250 sub: &SubscriptionRequest,
251 ) {
252 let client = match &inner.client {
253 Some(c) => c,
254 None => return,
255 };
256
257 let mut scopes_to_deliver: Vec<String> = Vec::new();
258 if sub.allscopes {
259 scopes_to_deliver.push(String::new());
261 } else {
262 for scope in &sub.scopes {
263 if !scope_has_star(scope) {
264 scopes_to_deliver.push(scope.clone());
265 }
266 }
267 }
268
269 let mut to_send: Vec<WaveEvent> = Vec::new();
270 for scope in scopes_to_deliver {
271 let key = (route_id.to_string(), sub.event.clone(), scope.clone());
272 if inner.replayed.contains(&key) {
273 continue;
274 }
275 let pkey = PersistKey {
276 event: sub.event.clone(),
277 scope: scope.clone(),
278 };
279 if let Some(pe) = inner.persist_map.get(&pkey) {
280 for event in &pe.events {
281 to_send.push(event.clone());
282 }
283 }
284 inner.replayed.insert(key);
285 }
286 for event in to_send {
287 client.send_event(route_id, event);
288 }
289 }
290
291 pub fn unsubscribe(&self, route_id: &str, event_name: &str) {
293 let mut inner = self.inner.lock().unwrap();
294 Self::unsubscribe_nolock(&mut inner, route_id, event_name);
295 }
296
297 pub fn unsubscribe_all(&self, route_id: &str) {
304 let mut inner = self.inner.lock().unwrap();
305 let events: Vec<String> = inner.sub_map.keys().cloned().collect();
306 for event in events {
307 Self::unsubscribe_nolock(&mut inner, route_id, &event);
308 }
309 inner.replayed.retain(|(r, _, _)| r != route_id);
310 }
311
312 fn unsubscribe_nolock(inner: &mut BrokerInner, route_id: &str, event_name: &str) {
313 let bs = match inner.sub_map.get_mut(event_name) {
314 Some(bs) => bs,
315 None => return,
316 };
317 bs.all_subs.retain(|s| s != route_id);
318 remove_from_all_scopes(&mut bs.scope_subs, route_id);
319 remove_from_all_scopes(&mut bs.star_subs, route_id);
320 if bs.is_empty() {
321 inner.sub_map.remove(event_name);
322 }
323 }
324
325 pub fn publish(&self, event: WaveEvent) {
327 let mut inner = self.inner.lock().unwrap();
328
329 if event.persist > 0 {
331 Self::persist_event(&mut inner, &event);
332 }
333
334 let client = match &inner.client {
335 Some(c) => c,
336 None => return,
337 };
338
339 let route_ids = Self::get_matching_routes(&inner, &event);
340 for route_id in route_ids {
341 client.send_event(&route_id, event.clone());
342 }
343 }
344
345 pub fn read_event_history(
347 &self,
348 event_type: &str,
349 scope: &str,
350 max_items: usize,
351 ) -> Vec<WaveEvent> {
352 if max_items == 0 {
353 return Vec::new();
354 }
355 let inner = self.inner.lock().unwrap();
356 let key = PersistKey {
357 event: event_type.to_string(),
358 scope: scope.to_string(),
359 };
360 match inner.persist_map.get(&key) {
361 Some(pe) if !pe.events.is_empty() => {
362 let n = max_items.min(pe.events.len());
363 pe.events[pe.events.len() - n..].to_vec()
364 }
365 _ => Vec::new(),
366 }
367 }
368
369 fn persist_event(inner: &mut BrokerInner, event: &WaveEvent) {
370 let num_persist = event.persist.min(MAX_PERSIST);
371 let mut scope_set: Vec<String> = event.scopes.clone();
372 scope_set.push(String::new()); for scope in scope_set {
375 let key = PersistKey {
376 event: event.event.clone(),
377 scope,
378 };
379 let pe = inner.persist_map.entry(key).or_insert_with(|| {
380 PersistEventWrap {
381 arr_total_adds: 0,
382 events: Vec::with_capacity(num_persist),
383 }
384 });
385 pe.events.push(event.clone());
386 pe.arr_total_adds += 1;
387 if pe.events.len() > num_persist {
389 pe.events.drain(..pe.events.len() - num_persist);
390 }
391 if pe.arr_total_adds > REMAKE_ARR_THRESHOLD {
393 let compacted: Vec<WaveEvent> = pe.events.drain(..).collect();
394 pe.events = compacted;
395 pe.arr_total_adds = pe.events.len();
396 }
397 }
398 }
399
400 fn get_matching_routes(inner: &BrokerInner, event: &WaveEvent) -> Vec<String> {
401 let bs = match inner.sub_map.get(&event.event) {
402 Some(bs) => bs,
403 None => return Vec::new(),
404 };
405
406 let mut route_ids: HashMap<&str, ()> = HashMap::new();
407
408 for route_id in &bs.all_subs {
410 route_ids.insert(route_id, ());
411 }
412
413 for scope in &event.scopes {
415 if let Some(routes) = bs.scope_subs.get(scope) {
416 for route_id in routes {
417 route_ids.insert(route_id, ());
418 }
419 }
420 for (star_scope, routes) in &bs.star_subs {
422 if star_match(star_scope, scope, ":") {
423 for route_id in routes {
424 route_ids.insert(route_id, ());
425 }
426 }
427 }
428 }
429
430 route_ids.keys().map(|s| s.to_string()).collect()
431 }
432}
433
434impl Default for Broker {
435 fn default() -> Self {
436 Self::new()
437 }
438}
439
440fn scope_has_star(scope: &str) -> bool {
443 scope.split(':').any(|part| part == "*" || part == "**")
444}
445
446fn star_match(pattern: &str, value: &str, sep: &str) -> bool {
449 let pat_parts: Vec<&str> = pattern.split(sep).collect();
450 let val_parts: Vec<&str> = value.split(sep).collect();
451
452 let mut pi = 0;
453 let mut vi = 0;
454 while pi < pat_parts.len() && vi < val_parts.len() {
455 if pat_parts[pi] == "**" {
456 return true; }
458 if pat_parts[pi] != "*" && pat_parts[pi] != val_parts[vi] {
459 return false;
460 }
461 pi += 1;
462 vi += 1;
463 }
464 pi == pat_parts.len() && vi == val_parts.len()
465}
466
467fn add_unique(vec: &mut Vec<String>, val: &str) {
468 if !vec.iter().any(|s| s == val) {
469 vec.push(val.to_string());
470 }
471}
472
473fn add_to_scope_map(map: &mut HashMap<String, Vec<String>>, scope: &str, route_id: &str) {
474 let entry = map.entry(scope.to_string()).or_default();
475 add_unique(entry, route_id);
476}
477
478fn remove_from_all_scopes(map: &mut HashMap<String, Vec<String>>, route_id: &str) {
479 let empty_scopes: Vec<String> = map
480 .iter_mut()
481 .filter_map(|(scope, routes)| {
482 routes.retain(|r| r != route_id);
483 if routes.is_empty() {
484 Some(scope.clone())
485 } else {
486 None
487 }
488 })
489 .collect();
490 for scope in empty_scopes {
491 map.remove(&scope);
492 }
493}
494
495pub fn publish_install_progress(broker: &Broker, block_id: &str, message: &str) {
499 let scope = format!("block:{}", block_id);
500 broker.publish(WaveEvent {
501 event: EVENT_INSTALL_PROGRESS.to_string(),
502 scopes: vec![scope],
503 sender: String::new(),
504 persist: 0,
505 data: Some(serde_json::json!({ "message": message })),
506 });
507}
508
509pub fn publish_block_activity(broker: &Broker, block_id: &str, activity: &str) {
514 let scope = format!("block:{}", block_id);
515 broker.publish(WaveEvent {
516 event: EVENT_BLOCK_ACTIVITY.to_string(),
517 scopes: vec![scope],
518 sender: String::new(),
519 persist: 0,
520 data: Some(serde_json::json!({ "blockId": block_id, "activity": activity })),
521 });
522}
523
524#[cfg(test)]
529mod tests {
530 use super::*;
531 use std::sync::Arc;
532
533 struct TestClient {
534 events: Mutex<Vec<(String, WaveEvent)>>,
535 }
536
537 impl TestClient {
538 fn new() -> Self {
539 Self {
540 events: Mutex::new(Vec::new()),
541 }
542 }
543
544 fn received_events(&self) -> Vec<(String, WaveEvent)> {
545 self.events.lock().unwrap().clone()
546 }
547 }
548
549 impl WpsClient for TestClient {
550 fn send_event(&self, route_id: &str, event: WaveEvent) {
551 self.events
552 .lock()
553 .unwrap()
554 .push((route_id.to_string(), event));
555 }
556 }
557
558 impl WpsClient for Arc<TestClient> {
559 fn send_event(&self, route_id: &str, event: WaveEvent) {
560 self.events
561 .lock()
562 .unwrap()
563 .push((route_id.to_string(), event));
564 }
565 }
566
567 #[test]
568 fn test_subscribe_all_scopes() {
569 let broker = Broker::new();
570 let client = Arc::new(TestClient::new());
571 broker.set_client(Box::new(Arc::clone(&client)));
572
573 broker.subscribe(
574 "route-1",
575 SubscriptionRequest {
576 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
577 scopes: vec![],
578 allscopes: true,
579 },
580 );
581
582 broker.publish(WaveEvent {
583 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
584 scopes: vec!["block:abc".to_string()],
585 sender: String::new(),
586 persist: 0,
587 data: None,
588 });
589
590 let events = client.received_events();
591 assert_eq!(events.len(), 1);
592 assert_eq!(events[0].0, "route-1");
593 }
594
595 #[test]
596 fn test_subscribe_exact_scope() {
597 let broker = Broker::new();
598 let client = Arc::new(TestClient::new());
599 broker.set_client(Box::new(Arc::clone(&client)));
600
601 broker.subscribe(
602 "route-1",
603 SubscriptionRequest {
604 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
605 scopes: vec!["block:abc".to_string()],
606 allscopes: false,
607 },
608 );
609
610 broker.publish(WaveEvent {
612 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
613 scopes: vec!["block:abc".to_string()],
614 sender: String::new(),
615 persist: 0,
616 data: None,
617 });
618
619 broker.publish(WaveEvent {
621 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
622 scopes: vec!["block:xyz".to_string()],
623 sender: String::new(),
624 persist: 0,
625 data: None,
626 });
627
628 let events = client.received_events();
629 assert_eq!(events.len(), 1);
630 }
631
632 #[test]
633 fn test_subscribe_star_scope() {
634 let broker = Broker::new();
635 let client = Arc::new(TestClient::new());
636 broker.set_client(Box::new(Arc::clone(&client)));
637
638 broker.subscribe(
639 "route-1",
640 SubscriptionRequest {
641 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
642 scopes: vec!["block:*".to_string()],
643 allscopes: false,
644 },
645 );
646
647 broker.publish(WaveEvent {
648 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
649 scopes: vec!["block:abc".to_string()],
650 sender: String::new(),
651 persist: 0,
652 data: None,
653 });
654
655 broker.publish(WaveEvent {
656 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
657 scopes: vec!["tab:xyz".to_string()],
658 sender: String::new(),
659 persist: 0,
660 data: None,
661 });
662
663 let events = client.received_events();
664 assert_eq!(events.len(), 1); }
666
667 #[test]
668 fn test_unsubscribe() {
669 let broker = Broker::new();
670 let client = Arc::new(TestClient::new());
671 broker.set_client(Box::new(Arc::clone(&client)));
672
673 broker.subscribe(
674 "route-1",
675 SubscriptionRequest {
676 event: EVENT_BLOCK_CLOSE.to_string(),
677 scopes: vec![],
678 allscopes: true,
679 },
680 );
681
682 broker.unsubscribe("route-1", EVENT_BLOCK_CLOSE);
683
684 broker.publish(WaveEvent {
685 event: EVENT_BLOCK_CLOSE.to_string(),
686 scopes: vec![],
687 sender: String::new(),
688 persist: 0,
689 data: None,
690 });
691
692 assert!(client.received_events().is_empty());
693 }
694
695 #[test]
696 fn test_unsubscribe_all() {
697 let broker = Broker::new();
698 let client = Arc::new(TestClient::new());
699 broker.set_client(Box::new(Arc::clone(&client)));
700
701 broker.subscribe(
702 "route-1",
703 SubscriptionRequest {
704 event: EVENT_BLOCK_CLOSE.to_string(),
705 scopes: vec![],
706 allscopes: true,
707 },
708 );
709 broker.subscribe(
710 "route-1",
711 SubscriptionRequest {
712 event: EVENT_CONFIG.to_string(),
713 scopes: vec![],
714 allscopes: true,
715 },
716 );
717
718 broker.unsubscribe_all("route-1");
719
720 broker.publish(WaveEvent {
721 event: EVENT_BLOCK_CLOSE.to_string(),
722 scopes: vec![],
723 sender: String::new(),
724 persist: 0,
725 data: None,
726 });
727 broker.publish(WaveEvent {
728 event: EVENT_CONFIG.to_string(),
729 scopes: vec![],
730 sender: String::new(),
731 persist: 0,
732 data: None,
733 });
734
735 assert!(client.received_events().is_empty());
736 }
737
738 #[test]
742 fn test_replay_on_subscribe_exact_scope() {
743 let broker = Broker::new();
744 let client = Arc::new(TestClient::new());
745 broker.set_client(Box::new(Arc::clone(&client)));
746
747 for i in 0..5 {
749 broker.publish(WaveEvent {
750 event: "tool_chunk".to_string(),
751 scopes: vec!["block:abc".to_string()],
752 sender: String::new(),
753 persist: 10,
754 data: Some(serde_json::json!({"tool_id": "t1", "n": i})),
755 });
756 }
757 assert!(
758 client.received_events().is_empty(),
759 "no subscriber yet, no delivery"
760 );
761
762 broker.subscribe(
764 "route-1",
765 SubscriptionRequest {
766 event: "tool_chunk".to_string(),
767 scopes: vec!["block:abc".to_string()],
768 allscopes: false,
769 },
770 );
771
772 let events = client.received_events();
773 assert_eq!(events.len(), 5, "all 5 persisted events replayed");
774 assert_eq!(events[0].1.data, Some(serde_json::json!({"tool_id": "t1", "n": 0})));
775 assert_eq!(events[4].1.data, Some(serde_json::json!({"tool_id": "t1", "n": 4})));
776 }
777
778 #[test]
784 fn test_replay_on_resubscribe_is_idempotent() {
785 let broker = Broker::new();
786 let client = Arc::new(TestClient::new());
787 broker.set_client(Box::new(Arc::clone(&client)));
788
789 for i in 0..3 {
790 broker.publish(WaveEvent {
791 event: "tool_chunk".to_string(),
792 scopes: vec!["block:abc".to_string()],
793 sender: String::new(),
794 persist: 10,
795 data: Some(serde_json::json!({"n": i})),
796 });
797 }
798
799 let sub = SubscriptionRequest {
800 event: "tool_chunk".to_string(),
801 scopes: vec!["block:abc".to_string()],
802 allscopes: false,
803 };
804
805 broker.subscribe("route-1", sub.clone());
806 assert_eq!(
807 client.received_events().len(),
808 3,
809 "first subscribe replays all 3 persisted events"
810 );
811
812 broker.subscribe("route-1", sub.clone());
815 assert_eq!(
816 client.received_events().len(),
817 3,
818 "re-subscribe is a no-op for replay; received count stays at 3"
819 );
820
821 broker.publish(WaveEvent {
823 event: "tool_chunk".to_string(),
824 scopes: vec!["block:abc".to_string()],
825 sender: String::new(),
826 persist: 10,
827 data: Some(serde_json::json!({"n": "live"})),
828 });
829 assert_eq!(
830 client.received_events().len(),
831 4,
832 "live publish after resubscribe is delivered exactly once"
833 );
834
835 broker.unsubscribe_all("route-1");
837 broker.subscribe("route-1", sub.clone());
838 assert_eq!(
839 client.received_events().len(),
840 8,
841 "reconnect after unsubscribe_all clears the replayed tracker; \
842 gets all 4 persisted events again"
843 );
844 }
845
846 #[test]
849 fn test_replay_on_subscribe_scope_isolation() {
850 let broker = Broker::new();
851 let client = Arc::new(TestClient::new());
852 broker.set_client(Box::new(Arc::clone(&client)));
853
854 broker.publish(WaveEvent {
855 event: "tool_chunk".to_string(),
856 scopes: vec!["block:xyz".to_string()],
857 sender: String::new(),
858 persist: 10,
859 data: Some(serde_json::json!({"tool_id": "other"})),
860 });
861
862 broker.subscribe(
863 "route-1",
864 SubscriptionRequest {
865 event: "tool_chunk".to_string(),
866 scopes: vec!["block:abc".to_string()],
867 allscopes: false,
868 },
869 );
870
871 let events = client.received_events();
872 assert_eq!(events.len(), 0, "scope:abc must not get block:xyz events");
873 }
874
875 #[test]
876 fn test_event_persistence() {
877 let broker = Broker::new();
878 let client = Arc::new(TestClient::new());
879 broker.set_client(Box::new(Arc::clone(&client)));
880
881 broker.subscribe(
883 "route-1",
884 SubscriptionRequest {
885 event: EVENT_SYS_INFO.to_string(),
886 scopes: vec![],
887 allscopes: true,
888 },
889 );
890
891 for i in 0..5 {
893 broker.publish(WaveEvent {
894 event: EVENT_SYS_INFO.to_string(),
895 scopes: vec!["cpu".to_string()],
896 sender: String::new(),
897 persist: 3, data: Some(serde_json::json!(i)),
899 });
900 }
901
902 let history = broker.read_event_history(EVENT_SYS_INFO, "", 10);
904 assert_eq!(history.len(), 3);
905 assert_eq!(history[0].data, Some(serde_json::json!(2)));
906 assert_eq!(history[2].data, Some(serde_json::json!(4)));
907
908 let scoped = broker.read_event_history(EVENT_SYS_INFO, "cpu", 2);
910 assert_eq!(scoped.len(), 2);
911 }
912
913 #[test]
914 fn test_star_match() {
915 assert!(star_match("block:*", "block:abc", ":"));
916 assert!(star_match("*:abc", "block:abc", ":"));
917 assert!(!star_match("block:*", "tab:abc", ":"));
918 assert!(star_match("**", "block:abc:xyz", ":"));
919 assert!(!star_match("block:*", "block:abc:xyz", ":")); }
921
922 #[test]
923 fn test_wave_event_serialization() {
924 let event = WaveEvent {
925 event: "test".to_string(),
926 scopes: vec!["scope1".to_string()],
927 sender: String::new(),
928 persist: 0,
929 data: Some(serde_json::json!({"key": "value"})),
930 };
931 let json = serde_json::to_string(&event).unwrap();
932 let parsed: WaveEvent = serde_json::from_str(&json).unwrap();
933 assert_eq!(parsed.event, "test");
934 assert_eq!(parsed.scopes, vec!["scope1"]);
935 assert!(!json.contains("\"sender\""));
937 assert!(!json.contains("\"persist\""));
938 }
939
940 #[test]
941 fn test_subscription_request_serialization() {
942 let req = SubscriptionRequest {
943 event: "blockclose".to_string(),
944 scopes: vec!["block:123".to_string()],
945 allscopes: false,
946 };
947 let json = serde_json::to_string(&req).unwrap();
948 let parsed: SubscriptionRequest = serde_json::from_str(&json).unwrap();
949 assert_eq!(parsed.event, "blockclose");
950 }
951
952 #[test]
953 fn test_no_client_publish_does_not_panic() {
954 let broker = Broker::new();
955 broker.publish(WaveEvent {
957 event: "test".to_string(),
958 scopes: vec![],
959 sender: String::new(),
960 persist: 0,
961 data: None,
962 });
963 }
964
965 #[test]
966 fn test_double_star_scope() {
967 let broker = Broker::new();
968 let client = Arc::new(TestClient::new());
969 broker.set_client(Box::new(Arc::clone(&client)));
970
971 broker.subscribe(
972 "route-1",
973 SubscriptionRequest {
974 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
975 scopes: vec!["**".to_string()],
976 allscopes: false,
977 },
978 );
979
980 broker.publish(WaveEvent {
981 event: EVENT_WAVE_OBJ_UPDATE.to_string(),
982 scopes: vec!["block:abc:def".to_string()],
983 sender: String::new(),
984 persist: 0,
985 data: None,
986 });
987
988 assert_eq!(client.received_events().len(), 1);
989 }
990}