1use std::collections::{HashMap, HashSet};
16use std::sync::Mutex;
17
18use serde::{Deserialize, Serialize};
19
20#[allow(dead_code)]
23pub const EVENT_BLOCK_CLOSE: &str = "blockclose";
24#[allow(dead_code)]
25pub const EVENT_CONN_CHANGE: &str = "connchange";
26pub const EVENT_REMOTES_CHANGE: &str = "remoteschange";
29pub const EVENT_WIDGET_PACKAGES: &str = "widgetpackages";
32pub const EVENT_WIDGET_STORAGE: &str = "widgetstorage";
34pub const EVENT_WIDGET_REQUESTS: &str = "widgetrequests";
36pub const EVENT_USER_ATTENTION: &str = "userattention";
39pub const EVENT_SYS_INFO: &str = "sysinfo";
40pub const EVENT_CONTROLLER_STATUS: &str = "controllerstatus";
41pub const EVENT_AGENT_RUNTIME: &str = "agentruntime";
43pub const EVENT_AGENT_CONTEXT_USAGE: &str = "agentcontextusage";
46pub const EVENT_AGENT_TURN: &str = "agentturn";
49pub const EVENT_MUX_OBJ_UPDATE: &str = "waveobj:update";
50pub const EVENT_BLOCK_FILE: &str = "blockfile";
51pub const EVENT_INSTALL_PROGRESS: &str = "install_progress";
52#[allow(dead_code)]
53pub const EVENT_CONFIG: &str = "config";
54#[allow(dead_code)]
55pub const EVENT_USER_INPUT: &str = "userinput";
56pub const EVENT_AGENT_MESSAGE_ACCEPTED: &str = "agent-message-accepted";
61pub const EVENT_ATTACHMENT_PROGRESS: &str = "attachment:progress";
66pub const EVENT_ATTACHMENT_READY: &str = "attachment:ready";
67pub const EVENT_ATTACHMENT_FAILED: &str = "attachment:failed";
68pub const EVENT_ATTACHMENT_BATCH_DONE: &str = "attachment:batch-done";
69#[allow(dead_code)]
70pub const EVENT_ROUTE_GONE: &str = "route:gone";
71pub const EVENT_BLOCK_STATS: &str = "blockstats";
72pub const EVENT_AGENT_FAILURE: &str = "agentfailure";
76pub const EVENT_AGENT_RESUME_RETRY: &str = "agent-resume-retry";
91pub const EVENT_SUBAGENT_BACKFILL_STATUS: &str = "subagent:backfill_status";
104pub const EVENT_SHELL_NODE_CREATE: &str = "shell_node_create";
108pub const EVENT_SHELL_CHUNK: &str = "shell_chunk";
112pub const EVENT_BLOCK_ACTIVITY: &str = "block:activity";
117pub const EVENT_CRON_CHANGED: &str = "cron_changed";
123pub const EVENT_DOCK_CLEAR: &str = "dock:clear";
132pub const EVENT_BTW_ANSWER_CHUNK: &str = "btw_answer_chunk";
151
152#[allow(dead_code)]
154pub const FILE_OP_CREATE: &str = "create";
155#[allow(dead_code)]
156pub const FILE_OP_DELETE: &str = "delete";
157pub const FILE_OP_APPEND: &str = "append";
158pub const FILE_OP_TRUNCATE: &str = "truncate";
159pub const FILE_OP_REPLACE: &str = "replace";
162#[allow(dead_code)]
163pub const FILE_OP_INVALIDATE: &str = "invalidate";
164
165const MAX_PERSIST: usize = 4096;
166const REMAKE_ARR_THRESHOLD: usize = 10 * 1024;
167
168#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
174#[ts(export, export_to = "../../../frontend/types/rpc/")]
175pub struct MuxEvent {
176 pub event: String,
177 #[serde(skip_serializing_if = "Vec::is_empty", default)]
179 #[ts(as = "Option<Vec<String>>", optional)]
180 pub scopes: Vec<String>,
181 #[serde(skip_serializing_if = "String::is_empty", default)]
182 #[ts(as = "Option<String>", optional)]
183 pub sender: String,
184 #[serde(skip_serializing_if = "is_zero", default)]
185 #[ts(as = "Option<u32>", optional)]
186 pub persist: usize,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 #[ts(optional, type = "any")]
189 pub data: Option<serde_json::Value>,
190}
191
192fn is_zero(v: &usize) -> bool {
193 *v == 0
194}
195
196impl MuxEvent {
197 #[allow(dead_code)]
198 pub fn has_scope(&self, scope: &str) -> bool {
199 self.scopes.iter().any(|s| s == scope)
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
204#[ts(export, export_to = "../../../frontend/types/rpc/")]
205pub struct SubscriptionRequest {
206 pub event: String,
207 #[serde(skip_serializing_if = "Vec::is_empty", default)]
208 pub scopes: Vec<String>,
209 #[serde(default)]
210 pub allscopes: bool,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)]
214#[ts(export, export_to = "../../../frontend/types/rpc/")]
215pub struct WSFileEventData {
216 pub zoneid: String,
217 pub filename: String,
218 pub fileop: String,
219 #[serde(skip_serializing_if = "String::is_empty", default)]
220 #[ts(as = "Option<String>", optional)]
221 pub data64: String,
222 #[serde(skip_serializing_if = "Option::is_none", default)]
228 #[ts(optional, type = "number")]
229 pub offset: Option<u64>,
230 #[serde(skip_serializing_if = "Vec::is_empty", default)]
236 #[ts(as = "Option<Vec<StreamPos>>", optional)]
237 pub pos: Vec<StreamPos>,
238 #[serde(skip_serializing_if = "Option::is_none", default)]
244 #[ts(optional)]
245 pub echo: Option<String>,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
251#[ts(export, export_to = "../../../frontend/types/rpc/")]
252pub struct StreamPos {
253 pub stream: String,
256 pub gen: String,
257 #[ts(type = "number")]
259 pub line: u64,
260 #[ts(type = "number")]
262 pub lines: u64,
263}
264
265pub trait WpsClient: Send + Sync {
269 fn send_event(&self, route_id: &str, event: MuxEvent);
270}
271
272#[derive(Default)]
275struct BrokerSubscription {
276 all_subs: Vec<String>,
278 scope_subs: HashMap<String, Vec<String>>,
280 star_subs: HashMap<String, Vec<String>>,
282}
283
284impl BrokerSubscription {
285 fn is_empty(&self) -> bool {
286 self.all_subs.is_empty() && self.scope_subs.is_empty() && self.star_subs.is_empty()
287 }
288}
289
290#[derive(Hash, Eq, PartialEq, Clone)]
291struct PersistKey {
292 event: String,
293 scope: String,
294}
295
296struct PersistEventWrap {
297 arr_total_adds: usize,
298 events: Vec<MuxEvent>,
299}
300
301pub struct Broker {
305 inner: Mutex<BrokerInner>,
306 observers: std::sync::RwLock<Vec<BrokerObserver>>,
310}
311
312pub type BrokerObserver = std::sync::Arc<dyn Fn(&MuxEvent) + Send + Sync>;
314
315type ReplayKey = (String, String, String);
322
323struct BrokerInner {
324 client: Option<Box<dyn WpsClient>>,
325 sub_map: HashMap<String, BrokerSubscription>,
326 persist_map: HashMap<PersistKey, PersistEventWrap>,
327 replayed: HashSet<ReplayKey>,
328}
329
330impl Broker {
331 pub fn new() -> Self {
332 Self {
333 inner: Mutex::new(BrokerInner {
334 client: None,
335 sub_map: HashMap::new(),
336 persist_map: HashMap::new(),
337 replayed: HashSet::new(),
338 }),
339 observers: std::sync::RwLock::new(Vec::new()),
340 }
341 }
342
343 pub fn add_observer(&self, f: BrokerObserver) {
347 self.observers.write().unwrap_or_else(|e| e.into_inner()).push(f);
348 }
349
350 pub fn set_client(&self, client: Box<dyn WpsClient>) {
351 let mut inner = self.inner.lock().unwrap();
352 inner.client = Some(client);
353 }
354
355 pub fn subscribe(&self, route_id: &str, sub: SubscriptionRequest) {
364 if sub.event.is_empty() {
365 return;
366 }
367 let mut inner = self.inner.lock().unwrap();
368 Self::unsubscribe_nolock(&mut inner, route_id, &sub.event);
370
371 let bs = inner
372 .sub_map
373 .entry(sub.event.clone())
374 .or_default();
375
376 if sub.allscopes {
377 add_unique(&mut bs.all_subs, route_id);
378 } else {
379 for scope in &sub.scopes {
380 if scope_has_star(scope) {
381 add_to_scope_map(&mut bs.star_subs, scope, route_id);
382 } else {
383 add_to_scope_map(&mut bs.scope_subs, scope, route_id);
384 }
385 }
386 }
387
388 Self::replay_to_route(&mut inner, route_id, &sub);
389 }
390
391 fn replay_to_route(
410 inner: &mut BrokerInner,
411 route_id: &str,
412 sub: &SubscriptionRequest,
413 ) {
414 let client = match &inner.client {
415 Some(c) => c,
416 None => return,
417 };
418
419 let mut scopes_to_deliver: Vec<String> = Vec::new();
420 if sub.allscopes {
421 scopes_to_deliver.push(String::new());
423 } else {
424 for scope in &sub.scopes {
425 if !scope_has_star(scope) {
426 scopes_to_deliver.push(scope.clone());
427 }
428 }
429 }
430
431 let mut to_send: Vec<MuxEvent> = Vec::new();
432 for scope in scopes_to_deliver {
433 let key = (route_id.to_string(), sub.event.clone(), scope.clone());
434 if inner.replayed.contains(&key) {
435 continue;
436 }
437 let pkey = PersistKey {
438 event: sub.event.clone(),
439 scope: scope.clone(),
440 };
441 if let Some(pe) = inner.persist_map.get(&pkey) {
442 for event in &pe.events {
443 to_send.push(event.clone());
444 }
445 }
446 inner.replayed.insert(key);
447 }
448 for event in to_send {
449 client.send_event(route_id, event);
450 }
451 }
452
453 pub fn has_subscribers(&self, event_name: &str) -> bool {
456 let inner = self.inner.lock().unwrap();
457 inner.sub_map.get(event_name).is_some_and(|bs| !bs.is_empty())
458 }
459
460 pub fn unsubscribe(&self, route_id: &str, event_name: &str) {
462 let mut inner = self.inner.lock().unwrap();
463 Self::unsubscribe_nolock(&mut inner, route_id, event_name);
464 }
465
466 pub fn unsubscribe_all(&self, route_id: &str) {
473 let mut inner = self.inner.lock().unwrap();
474 let events: Vec<String> = inner.sub_map.keys().cloned().collect();
475 for event in events {
476 Self::unsubscribe_nolock(&mut inner, route_id, &event);
477 }
478 inner.replayed.retain(|(r, _, _)| r != route_id);
479 }
480
481 pub fn purge_scope(&self, scope: &str) {
489 let mut inner = self.inner.lock().unwrap();
490 inner.persist_map.retain(|k, _| k.scope != scope);
491 inner.replayed.retain(|(_, _, s)| s != scope);
492 }
493
494 fn unsubscribe_nolock(inner: &mut BrokerInner, route_id: &str, event_name: &str) {
495 let bs = match inner.sub_map.get_mut(event_name) {
496 Some(bs) => bs,
497 None => return,
498 };
499 bs.all_subs.retain(|s| s != route_id);
500 remove_from_all_scopes(&mut bs.scope_subs, route_id);
501 remove_from_all_scopes(&mut bs.star_subs, route_id);
502 if bs.is_empty() {
503 inner.sub_map.remove(event_name);
504 }
505 }
506
507 pub fn publish_event<E: super::mps_events::Event>(&self, payload: &E::Payload, scopes: Vec<String>, persist: usize) {
510 self.publish(MuxEvent {
512 event: E::NAME.to_string(),
513 scopes,
514 sender: String::new(),
515 persist,
516 data: super::mps_events::payload_data(payload),
517 });
518 }
519
520 pub fn publish_event_for<E: super::mps_events::PerIdEvent>(&self, id: &str, payload: &E::Payload, scopes: Vec<String>, persist: usize) {
522 self.publish(MuxEvent {
524 event: format!("{}{id}", E::PREFIX),
525 scopes,
526 sender: String::new(),
527 persist,
528 data: super::mps_events::payload_data(payload),
529 });
530 }
531
532 pub fn publish(&self, event: MuxEvent) {
534 for o in self.observers.read().unwrap_or_else(|e| e.into_inner()).iter() {
535 o(&event);
536 }
537 let mut inner = self.inner.lock().unwrap();
538
539 if event.persist > 0 {
541 Self::persist_event(&mut inner, &event);
542 }
543
544 let client = match &inner.client {
545 Some(c) => c,
546 None => return,
547 };
548
549 let route_ids = Self::get_matching_routes(&inner, &event);
550 for route_id in route_ids {
551 client.send_event(&route_id, event.clone());
552 }
553 }
554
555 pub fn read_event_history(
557 &self,
558 event_type: &str,
559 scope: &str,
560 max_items: usize,
561 ) -> Vec<MuxEvent> {
562 if max_items == 0 {
563 return Vec::new();
564 }
565 let inner = self.inner.lock().unwrap();
566 let key = PersistKey {
567 event: event_type.to_string(),
568 scope: scope.to_string(),
569 };
570 match inner.persist_map.get(&key) {
571 Some(pe) if !pe.events.is_empty() => {
572 let n = max_items.min(pe.events.len());
573 pe.events[pe.events.len() - n..].to_vec()
574 }
575 _ => Vec::new(),
576 }
577 }
578
579 fn persist_event(inner: &mut BrokerInner, event: &MuxEvent) {
580 let num_persist = event.persist.min(MAX_PERSIST);
581 let mut scope_set: Vec<String> = event.scopes.clone();
582 scope_set.push(String::new()); for scope in scope_set {
585 let key = PersistKey {
586 event: event.event.clone(),
587 scope,
588 };
589 let pe = inner.persist_map.entry(key).or_insert_with(|| {
590 PersistEventWrap {
591 arr_total_adds: 0,
592 events: Vec::with_capacity(num_persist),
593 }
594 });
595 pe.events.push(event.clone());
596 pe.arr_total_adds += 1;
597 if pe.events.len() > num_persist {
599 pe.events.drain(..pe.events.len() - num_persist);
600 }
601 if pe.arr_total_adds > REMAKE_ARR_THRESHOLD {
603 let compacted: Vec<MuxEvent> = pe.events.drain(..).collect();
604 pe.events = compacted;
605 pe.arr_total_adds = pe.events.len();
606 }
607 }
608 }
609
610 fn get_matching_routes(inner: &BrokerInner, event: &MuxEvent) -> Vec<String> {
611 let bs = match inner.sub_map.get(&event.event) {
612 Some(bs) => bs,
613 None => return Vec::new(),
614 };
615
616 let mut route_ids: HashMap<&str, ()> = HashMap::new();
617
618 for route_id in &bs.all_subs {
620 route_ids.insert(route_id, ());
621 }
622
623 for scope in &event.scopes {
625 if let Some(routes) = bs.scope_subs.get(scope) {
626 for route_id in routes {
627 route_ids.insert(route_id, ());
628 }
629 }
630 for (star_scope, routes) in &bs.star_subs {
632 if star_match(star_scope, scope, ":") {
633 for route_id in routes {
634 route_ids.insert(route_id, ());
635 }
636 }
637 }
638 }
639
640 route_ids.keys().map(|s| s.to_string()).collect()
641 }
642}
643
644impl Default for Broker {
645 fn default() -> Self {
646 Self::new()
647 }
648}
649
650fn scope_has_star(scope: &str) -> bool {
653 scope.split(':').any(|part| part == "*" || part == "**")
654}
655
656fn star_match(pattern: &str, value: &str, sep: &str) -> bool {
659 let pat_parts: Vec<&str> = pattern.split(sep).collect();
660 let val_parts: Vec<&str> = value.split(sep).collect();
661
662 let mut pi = 0;
663 let mut vi = 0;
664 while pi < pat_parts.len() && vi < val_parts.len() {
665 if pat_parts[pi] == "**" {
666 return true; }
668 if pat_parts[pi] != "*" && pat_parts[pi] != val_parts[vi] {
669 return false;
670 }
671 pi += 1;
672 vi += 1;
673 }
674 pi == pat_parts.len() && vi == val_parts.len()
675}
676
677fn add_unique(vec: &mut Vec<String>, val: &str) {
678 if !vec.iter().any(|s| s == val) {
679 vec.push(val.to_string());
680 }
681}
682
683fn add_to_scope_map(map: &mut HashMap<String, Vec<String>>, scope: &str, route_id: &str) {
684 let entry = map.entry(scope.to_string()).or_default();
685 add_unique(entry, route_id);
686}
687
688fn remove_from_all_scopes(map: &mut HashMap<String, Vec<String>>, route_id: &str) {
689 let empty_scopes: Vec<String> = map
690 .iter_mut()
691 .filter_map(|(scope, routes)| {
692 routes.retain(|r| r != route_id);
693 if routes.is_empty() {
694 Some(scope.clone())
695 } else {
696 None
697 }
698 })
699 .collect();
700 for scope in empty_scopes {
701 map.remove(&scope);
702 }
703}
704
705pub fn publish_install_progress(broker: &Broker, block_id: &str, message: &str) {
709 let scope = format!("block:{}", block_id);
710 let line = super::mps_events::InstallProgressEvent { message: message.to_string() };
711 broker.publish_event::<super::mps_events::InstallProgress>(&line, vec![scope], 0);
712}
713
714pub fn publish_block_activity(broker: &Broker, block_id: &str, activity: &str) {
719 let scope = format!("block:{}", block_id);
720 let event = super::mps_events::BlockActivityEvent { block_id: block_id.to_string(), activity: activity.to_string() };
721 broker.publish_event::<super::mps_events::BlockActivity>(&event, vec![scope], 0);
722}
723
724#[cfg(test)]
729mod tests {
730 #[test]
731 fn observers_see_every_publish_even_without_a_client() {
732 let b = Broker::new();
733 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
734 let s2 = seen.clone();
735 b.add_observer(std::sync::Arc::new(move |e: &MuxEvent| s2.lock().unwrap().push(e.event.clone())));
736 for name in ["agentfailure", "config"] {
737 b.publish(MuxEvent { event: name.into(), scopes: vec![], sender: String::new(), persist: 0, data: None });
738 }
739 assert_eq!(*seen.lock().unwrap(), vec!["agentfailure".to_string(), "config".to_string()]);
740 }
741
742 #[test]
743 fn a_signal_event_goes_out_with_no_data_and_a_payload_event_with_its_payload() {
744 use crate::backend::mps_events::{AgentsChanged, FilesChanged, FilesChangedEvent};
745 let b = Broker::new();
746 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::<MuxEvent>::new()));
747 let s2 = seen.clone();
748 b.add_observer(std::sync::Arc::new(move |e: &MuxEvent| s2.lock().unwrap().push(e.clone())));
749 b.publish_event::<AgentsChanged>(&(), vec![], 0);
750 b.publish_event::<FilesChanged>(&FilesChangedEvent { dir: "/tmp".into() }, vec![], 0);
751 let seen = seen.lock().unwrap();
752 let signal = serde_json::to_value(&seen[0]).unwrap();
754 assert_eq!(signal["event"], "agents:changed");
755 assert!(signal.get("data").is_none(), "{signal}");
756 assert_eq!(seen[1].data, Some(serde_json::json!({ "dir": "/tmp" })));
757 }
758
759 #[test]
760 fn a_per_id_event_goes_out_under_its_prefix_and_the_id() {
761 use crate::backend::mps_events::{AgentCredentialsRevoked, AgentMemoryChanged, CredentialsRevokedEvent};
762 let b = Broker::new();
763 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::<MuxEvent>::new()));
764 let s2 = seen.clone();
765 b.add_observer(std::sync::Arc::new(move |e: &MuxEvent| s2.lock().unwrap().push(e.clone())));
766 let revoked = CredentialsRevokedEvent { credentials_revoked: true, provider: "claude".into(), account_id: None };
767 b.publish_event_for::<AgentCredentialsRevoked>("agent-1", &revoked, vec![], 0);
768 b.publish_event_for::<AgentMemoryChanged>("agent-2", &(), vec![], 0);
769 let seen = seen.lock().unwrap();
770 assert_eq!(seen[0].event, "agentcredentials:revoked:agent-1");
771 assert_eq!(seen[0].data, Some(serde_json::json!({ "credentialsRevoked": true, "provider": "claude" })));
773 assert_eq!(seen[1].event, "agent:memory:changed:agent-2");
774 assert_eq!(seen[1].data, None);
775 }
776
777 use super::*;
778 use std::sync::Arc;
779
780 struct TestClient {
781 events: Mutex<Vec<(String, MuxEvent)>>,
782 }
783
784 impl TestClient {
785 fn new() -> Self {
786 Self {
787 events: Mutex::new(Vec::new()),
788 }
789 }
790
791 fn received_events(&self) -> Vec<(String, MuxEvent)> {
792 self.events.lock().unwrap().clone()
793 }
794 }
795
796 impl WpsClient for TestClient {
797 fn send_event(&self, route_id: &str, event: MuxEvent) {
798 self.events
799 .lock()
800 .unwrap()
801 .push((route_id.to_string(), event));
802 }
803 }
804
805 impl WpsClient for Arc<TestClient> {
806 fn send_event(&self, route_id: &str, event: MuxEvent) {
807 self.events
808 .lock()
809 .unwrap()
810 .push((route_id.to_string(), event));
811 }
812 }
813
814 #[test]
815 fn test_subscribe_all_scopes() {
816 let broker = Broker::new();
817 let client = Arc::new(TestClient::new());
818 broker.set_client(Box::new(Arc::clone(&client)));
819
820 broker.subscribe(
821 "route-1",
822 SubscriptionRequest {
823 event: EVENT_MUX_OBJ_UPDATE.to_string(),
824 scopes: vec![],
825 allscopes: true,
826 },
827 );
828
829 broker.publish(MuxEvent {
830 event: EVENT_MUX_OBJ_UPDATE.to_string(),
831 scopes: vec!["block:abc".to_string()],
832 sender: String::new(),
833 persist: 0,
834 data: None,
835 });
836
837 let events = client.received_events();
838 assert_eq!(events.len(), 1);
839 assert_eq!(events[0].0, "route-1");
840 }
841
842 #[test]
843 fn test_subscribe_exact_scope() {
844 let broker = Broker::new();
845 let client = Arc::new(TestClient::new());
846 broker.set_client(Box::new(Arc::clone(&client)));
847
848 broker.subscribe(
849 "route-1",
850 SubscriptionRequest {
851 event: EVENT_MUX_OBJ_UPDATE.to_string(),
852 scopes: vec!["block:abc".to_string()],
853 allscopes: false,
854 },
855 );
856
857 broker.publish(MuxEvent {
859 event: EVENT_MUX_OBJ_UPDATE.to_string(),
860 scopes: vec!["block:abc".to_string()],
861 sender: String::new(),
862 persist: 0,
863 data: None,
864 });
865
866 broker.publish(MuxEvent {
868 event: EVENT_MUX_OBJ_UPDATE.to_string(),
869 scopes: vec!["block:xyz".to_string()],
870 sender: String::new(),
871 persist: 0,
872 data: None,
873 });
874
875 let events = client.received_events();
876 assert_eq!(events.len(), 1);
877 }
878
879 #[test]
880 fn test_subscribe_star_scope() {
881 let broker = Broker::new();
882 let client = Arc::new(TestClient::new());
883 broker.set_client(Box::new(Arc::clone(&client)));
884
885 broker.subscribe(
886 "route-1",
887 SubscriptionRequest {
888 event: EVENT_MUX_OBJ_UPDATE.to_string(),
889 scopes: vec!["block:*".to_string()],
890 allscopes: false,
891 },
892 );
893
894 broker.publish(MuxEvent {
895 event: EVENT_MUX_OBJ_UPDATE.to_string(),
896 scopes: vec!["block:abc".to_string()],
897 sender: String::new(),
898 persist: 0,
899 data: None,
900 });
901
902 broker.publish(MuxEvent {
903 event: EVENT_MUX_OBJ_UPDATE.to_string(),
904 scopes: vec!["tab:xyz".to_string()],
905 sender: String::new(),
906 persist: 0,
907 data: None,
908 });
909
910 let events = client.received_events();
911 assert_eq!(events.len(), 1); }
913
914 #[test]
915 fn test_unsubscribe() {
916 let broker = Broker::new();
917 let client = Arc::new(TestClient::new());
918 broker.set_client(Box::new(Arc::clone(&client)));
919
920 broker.subscribe(
921 "route-1",
922 SubscriptionRequest {
923 event: EVENT_BLOCK_CLOSE.to_string(),
924 scopes: vec![],
925 allscopes: true,
926 },
927 );
928
929 broker.unsubscribe("route-1", EVENT_BLOCK_CLOSE);
930
931 broker.publish(MuxEvent {
932 event: EVENT_BLOCK_CLOSE.to_string(),
933 scopes: vec![],
934 sender: String::new(),
935 persist: 0,
936 data: None,
937 });
938
939 assert!(client.received_events().is_empty());
940 }
941
942 #[test]
943 fn test_unsubscribe_all() {
944 let broker = Broker::new();
945 let client = Arc::new(TestClient::new());
946 broker.set_client(Box::new(Arc::clone(&client)));
947
948 broker.subscribe(
949 "route-1",
950 SubscriptionRequest {
951 event: EVENT_BLOCK_CLOSE.to_string(),
952 scopes: vec![],
953 allscopes: true,
954 },
955 );
956 broker.subscribe(
957 "route-1",
958 SubscriptionRequest {
959 event: EVENT_CONFIG.to_string(),
960 scopes: vec![],
961 allscopes: true,
962 },
963 );
964
965 broker.unsubscribe_all("route-1");
966
967 broker.publish(MuxEvent {
968 event: EVENT_BLOCK_CLOSE.to_string(),
969 scopes: vec![],
970 sender: String::new(),
971 persist: 0,
972 data: None,
973 });
974 broker.publish(MuxEvent {
975 event: EVENT_CONFIG.to_string(),
976 scopes: vec![],
977 sender: String::new(),
978 persist: 0,
979 data: None,
980 });
981
982 assert!(client.received_events().is_empty());
983 }
984
985 #[test]
989 fn test_has_subscribers_follows_any_subscription_of_the_event() {
990 let broker = Broker::new();
991 assert!(!broker.has_subscribers(EVENT_BLOCK_STATS));
992 let sub = |event: &str, scopes: Vec<&str>, allscopes: bool| SubscriptionRequest {
993 event: event.to_string(),
994 scopes: scopes.into_iter().map(String::from).collect(),
995 allscopes,
996 };
997 broker.subscribe("route-1", sub(EVENT_BLOCK_STATS, vec!["block:abc"], false));
998 assert!(broker.has_subscribers(EVENT_BLOCK_STATS), "an exact scope counts");
999 assert!(!broker.has_subscribers(EVENT_SYS_INFO), "another event doesn't");
1000 broker.subscribe("route-2", sub(EVENT_BLOCK_STATS, vec![], true));
1001 broker.unsubscribe("route-1", EVENT_BLOCK_STATS);
1002 assert!(broker.has_subscribers(EVENT_BLOCK_STATS), "route-2 still watches every scope");
1003 broker.subscribe("route-3", sub(EVENT_BLOCK_STATS, vec!["block:*"], false));
1004 broker.unsubscribe_all("route-2");
1005 assert!(broker.has_subscribers(EVENT_BLOCK_STATS), "a wildcard scope counts");
1006 broker.unsubscribe_all("route-3");
1007 assert!(!broker.has_subscribers(EVENT_BLOCK_STATS));
1008 }
1009
1010 #[test]
1014 fn test_replay_on_subscribe_exact_scope() {
1015 let broker = Broker::new();
1016 let client = Arc::new(TestClient::new());
1017 broker.set_client(Box::new(Arc::clone(&client)));
1018
1019 for i in 0..5 {
1021 broker.publish(MuxEvent {
1022 event: "tool_chunk".to_string(),
1023 scopes: vec!["block:abc".to_string()],
1024 sender: String::new(),
1025 persist: 10,
1026 data: Some(serde_json::json!({"tool_id": "t1", "n": i})),
1027 });
1028 }
1029 assert!(
1030 client.received_events().is_empty(),
1031 "no subscriber yet, no delivery"
1032 );
1033
1034 broker.subscribe(
1036 "route-1",
1037 SubscriptionRequest {
1038 event: "tool_chunk".to_string(),
1039 scopes: vec!["block:abc".to_string()],
1040 allscopes: false,
1041 },
1042 );
1043
1044 let events = client.received_events();
1045 assert_eq!(events.len(), 5, "all 5 persisted events replayed");
1046 assert_eq!(events[0].1.data, Some(serde_json::json!({"tool_id": "t1", "n": 0})));
1047 assert_eq!(events[4].1.data, Some(serde_json::json!({"tool_id": "t1", "n": 4})));
1048 }
1049
1050 #[test]
1056 fn test_replay_on_resubscribe_is_idempotent() {
1057 let broker = Broker::new();
1058 let client = Arc::new(TestClient::new());
1059 broker.set_client(Box::new(Arc::clone(&client)));
1060
1061 for i in 0..3 {
1062 broker.publish(MuxEvent {
1063 event: "tool_chunk".to_string(),
1064 scopes: vec!["block:abc".to_string()],
1065 sender: String::new(),
1066 persist: 10,
1067 data: Some(serde_json::json!({"n": i})),
1068 });
1069 }
1070
1071 let sub = SubscriptionRequest {
1072 event: "tool_chunk".to_string(),
1073 scopes: vec!["block:abc".to_string()],
1074 allscopes: false,
1075 };
1076
1077 broker.subscribe("route-1", sub.clone());
1078 assert_eq!(
1079 client.received_events().len(),
1080 3,
1081 "first subscribe replays all 3 persisted events"
1082 );
1083
1084 broker.subscribe("route-1", sub.clone());
1087 assert_eq!(
1088 client.received_events().len(),
1089 3,
1090 "re-subscribe is a no-op for replay; received count stays at 3"
1091 );
1092
1093 broker.publish(MuxEvent {
1095 event: "tool_chunk".to_string(),
1096 scopes: vec!["block:abc".to_string()],
1097 sender: String::new(),
1098 persist: 10,
1099 data: Some(serde_json::json!({"n": "live"})),
1100 });
1101 assert_eq!(
1102 client.received_events().len(),
1103 4,
1104 "live publish after resubscribe is delivered exactly once"
1105 );
1106
1107 broker.unsubscribe_all("route-1");
1109 broker.subscribe("route-1", sub.clone());
1110 assert_eq!(
1111 client.received_events().len(),
1112 8,
1113 "reconnect after unsubscribe_all clears the replayed tracker; \
1114 gets all 4 persisted events again"
1115 );
1116 }
1117
1118 #[test]
1121 fn test_replay_on_subscribe_scope_isolation() {
1122 let broker = Broker::new();
1123 let client = Arc::new(TestClient::new());
1124 broker.set_client(Box::new(Arc::clone(&client)));
1125
1126 broker.publish(MuxEvent {
1127 event: "tool_chunk".to_string(),
1128 scopes: vec!["block:xyz".to_string()],
1129 sender: String::new(),
1130 persist: 10,
1131 data: Some(serde_json::json!({"tool_id": "other"})),
1132 });
1133
1134 broker.subscribe(
1135 "route-1",
1136 SubscriptionRequest {
1137 event: "tool_chunk".to_string(),
1138 scopes: vec!["block:abc".to_string()],
1139 allscopes: false,
1140 },
1141 );
1142
1143 let events = client.received_events();
1144 assert_eq!(events.len(), 0, "scope:abc must not get block:xyz events");
1145 }
1146
1147 #[test]
1148 fn test_event_persistence() {
1149 let broker = Broker::new();
1150 let client = Arc::new(TestClient::new());
1151 broker.set_client(Box::new(Arc::clone(&client)));
1152
1153 broker.subscribe(
1155 "route-1",
1156 SubscriptionRequest {
1157 event: EVENT_SYS_INFO.to_string(),
1158 scopes: vec![],
1159 allscopes: true,
1160 },
1161 );
1162
1163 for i in 0..5 {
1165 broker.publish(MuxEvent {
1166 event: EVENT_SYS_INFO.to_string(),
1167 scopes: vec!["cpu".to_string()],
1168 sender: String::new(),
1169 persist: 3, data: Some(serde_json::json!(i)),
1171 });
1172 }
1173
1174 let history = broker.read_event_history(EVENT_SYS_INFO, "", 10);
1176 assert_eq!(history.len(), 3);
1177 assert_eq!(history[0].data, Some(serde_json::json!(2)));
1178 assert_eq!(history[2].data, Some(serde_json::json!(4)));
1179
1180 let scoped = broker.read_event_history(EVENT_SYS_INFO, "cpu", 2);
1182 assert_eq!(scoped.len(), 2);
1183 }
1184
1185 #[test]
1186 fn test_purge_scope_removes_only_matching_scope() {
1187 let broker = Broker::new();
1188
1189 broker.publish(MuxEvent {
1193 event: "install_progress".to_string(),
1194 scopes: vec!["block:abc".to_string()],
1195 sender: String::new(),
1196 persist: 5,
1197 data: Some(serde_json::json!("a")),
1198 });
1199 broker.publish(MuxEvent {
1200 event: EVENT_BLOCK_ACTIVITY.to_string(),
1201 scopes: vec!["block:abc".to_string()],
1202 sender: String::new(),
1203 persist: 5,
1204 data: Some(serde_json::json!("b")),
1205 });
1206 broker.publish(MuxEvent {
1207 event: "install_progress".to_string(),
1208 scopes: vec!["block:xyz".to_string()],
1209 sender: String::new(),
1210 persist: 5,
1211 data: Some(serde_json::json!("c")),
1212 });
1213
1214 assert_eq!(broker.read_event_history("install_progress", "block:abc", 10).len(), 1);
1215 assert_eq!(broker.read_event_history(EVENT_BLOCK_ACTIVITY, "block:abc", 10).len(), 1);
1216 assert_eq!(broker.read_event_history("install_progress", "block:xyz", 10).len(), 1);
1217
1218 broker.purge_scope("block:abc");
1219
1220 assert_eq!(broker.read_event_history("install_progress", "block:abc", 10).len(), 0);
1222 assert_eq!(broker.read_event_history(EVENT_BLOCK_ACTIVITY, "block:abc", 10).len(), 0);
1223 assert_eq!(broker.read_event_history("install_progress", "block:xyz", 10).len(), 1);
1225 }
1226
1227 #[test]
1228 fn test_purge_scope_on_unknown_scope_is_noop() {
1229 let broker = Broker::new();
1231 broker.purge_scope("block:never-existed");
1232 }
1233
1234 #[test]
1235 fn test_star_match() {
1236 assert!(star_match("block:*", "block:abc", ":"));
1237 assert!(star_match("*:abc", "block:abc", ":"));
1238 assert!(!star_match("block:*", "tab:abc", ":"));
1239 assert!(star_match("**", "block:abc:xyz", ":"));
1240 assert!(!star_match("block:*", "block:abc:xyz", ":")); }
1242
1243 #[test]
1244 fn test_mux_event_serialization() {
1245 let event = MuxEvent {
1246 event: "test".to_string(),
1247 scopes: vec!["scope1".to_string()],
1248 sender: String::new(),
1249 persist: 0,
1250 data: Some(serde_json::json!({"key": "value"})),
1251 };
1252 let json = serde_json::to_string(&event).unwrap();
1253 let parsed: MuxEvent = serde_json::from_str(&json).unwrap();
1254 assert_eq!(parsed.event, "test");
1255 assert_eq!(parsed.scopes, vec!["scope1"]);
1256 assert!(!json.contains("\"sender\""));
1258 assert!(!json.contains("\"persist\""));
1259 }
1260
1261 #[test]
1262 fn test_subscription_request_serialization() {
1263 let req = SubscriptionRequest {
1264 event: "blockclose".to_string(),
1265 scopes: vec!["block:123".to_string()],
1266 allscopes: false,
1267 };
1268 let json = serde_json::to_string(&req).unwrap();
1269 let parsed: SubscriptionRequest = serde_json::from_str(&json).unwrap();
1270 assert_eq!(parsed.event, "blockclose");
1271 }
1272
1273 #[test]
1274 fn test_no_client_publish_does_not_panic() {
1275 let broker = Broker::new();
1276 broker.publish(MuxEvent {
1278 event: "test".to_string(),
1279 scopes: vec![],
1280 sender: String::new(),
1281 persist: 0,
1282 data: None,
1283 });
1284 }
1285
1286 #[test]
1287 fn test_double_star_scope() {
1288 let broker = Broker::new();
1289 let client = Arc::new(TestClient::new());
1290 broker.set_client(Box::new(Arc::clone(&client)));
1291
1292 broker.subscribe(
1293 "route-1",
1294 SubscriptionRequest {
1295 event: EVENT_MUX_OBJ_UPDATE.to_string(),
1296 scopes: vec!["**".to_string()],
1297 allscopes: false,
1298 },
1299 );
1300
1301 broker.publish(MuxEvent {
1302 event: EVENT_MUX_OBJ_UPDATE.to_string(),
1303 scopes: vec!["block:abc:def".to_string()],
1304 sender: String::new(),
1305 persist: 0,
1306 data: None,
1307 });
1308
1309 assert_eq!(client.received_events().len(), 1);
1310 }
1311}