1use parking_lot::{Mutex, RwLock};
2use std::collections::{BTreeMap, HashMap};
3use std::future::Future;
4use std::pin::Pin;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::Arc;
7
8use crate::effect::Disposable;
9use crate::service::{CordisError, Service};
10use crate::EventId;
11
12#[derive(Debug)]
20pub struct AggregateError {
21 pub errors: Vec<(String, String)>,
22}
23
24impl std::fmt::Display for AggregateError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}", format_listener_errors(&self.errors))
27 }
28}
29
30impl std::error::Error for AggregateError {}
31
32pub fn summarize_listener_errors(errors: Vec<(String, String)>) -> String {
36 format_listener_errors(&errors)
37}
38
39fn format_listener_errors(errors: &[(String, String)]) -> String {
40 if errors.is_empty() {
41 return "0 listener failures".to_string();
42 }
43 let joined = errors
44 .iter()
45 .map(|(name, message)| format!("{name}: {message}"))
46 .collect::<Vec<_>>()
47 .join("; ");
48 if errors.len() == 1 {
49 format!("1 listener failure: {joined}")
50 } else {
51 format!("{} listener failures: {joined}", errors.len())
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub enum Dispatch {
57 Emit,
58 Parallel,
59 Serial,
60 Bail,
61 Waterfall,
62}
63
64#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
76pub struct EventOptions {
77 pub prepend: bool,
78 pub global: bool,
79}
80
81pub type ListenerFilter = Box<dyn Fn(&EventOptions) -> bool + Send + Sync>;
88
89pub const INTERNAL_GET_EVENT: &str = "internal/get";
94pub const INTERNAL_SET_EVENT: &str = "internal/set";
95pub const INTERNAL_CONFIG_EVENT: &str = "internal/config";
96pub const INTERNAL_UPDATE_EVENT: &str = "internal/update";
97pub const INTERNAL_LISTENER_EVENT: &str = "internal/listener";
98pub const INTERNAL_DISPATCH_EVENT: &str = "internal/dispatch";
99
100pub fn is_internal_meta_event(event: &str) -> bool {
102 matches!(
103 event,
104 INTERNAL_GET_EVENT
105 | INTERNAL_SET_EVENT
106 | INTERNAL_CONFIG_EVENT
107 | INTERNAL_UPDATE_EVENT
108 | INTERNAL_LISTENER_EVENT
109 | INTERNAL_DISPATCH_EVENT
110 )
111}
112
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
116pub struct InternalDispatchPayload {
117 pub mode: String,
119 pub name: String,
120 pub args: serde_json::Value,
121}
122
123const MECHANICS_TEST_EVENTS: &[&str] = &[
127 "test",
128 "test.event",
129 "gone",
130 "gone.wf",
131 "parallel.result",
132 "serial.bail",
133 "serial.identity",
134 "serial.test",
135 "bail.test",
136 "emit.test",
137 "emit.counter",
138 "wf.next",
139 "wf.short",
140 "wf.empty",
141 "par.test",
142 "par2.test",
143 "par.agg",
144 "par.solo",
145 "around.empty",
146 "around.wrap",
147 "around.short",
148 "once.test",
149 "once.bail",
150 "prepend.test",
152 "filtered.test",
153 "global.test",
154 "blocked.event",
156 "allowed.event",
157 "another.event",
158 "observed.a",
159 "observed.b",
160 "observed.c",
161 "wf.filtered",
162];
163
164fn bypasses_catalog(event: &str) -> bool {
165 (cfg!(test) && MECHANICS_TEST_EVENTS.contains(&event)) || is_internal_meta_event(event)
166}
167
168fn debug_enforce_dispatch(event: &EventId, mode: Dispatch) {
170 if bypasses_catalog(event) {
171 return;
172 }
173 if let Err(msg) = crate::events_catalog::validate_dispatch(event, mode) {
174 debug_assert!(false, "{msg}");
175 }
176}
177
178fn debug_enforce_listener(event: &EventId, waterfall_registration: bool) {
180 if bypasses_catalog(event) {
181 return;
182 }
183 if let Err(msg) = crate::events_catalog::validate_listener(event, waterfall_registration) {
184 debug_assert!(false, "{msg}");
185 }
186}
187
188impl std::fmt::Display for Dispatch {
189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190 let name = match self {
191 Dispatch::Emit => "emit",
192 Dispatch::Parallel => "parallel",
193 Dispatch::Serial => "serial",
194 Dispatch::Bail => "bail",
195 Dispatch::Waterfall => "waterfall",
196 };
197 f.write_str(name)
198 }
199}
200
201type Handler = Arc<
202 dyn Fn(
203 serde_json::Value,
204 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
205 + Send
206 + Sync,
207>;
208
209pub type WaterfallNext = Box<
214 dyn FnOnce(
215 serde_json::Value,
216 )
217 -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
218 + Send,
219>;
220
221type WaterfallHandler = Arc<
226 dyn Fn(
227 serde_json::Value,
228 WaterfallNext,
229 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
230 + Send
231 + Sync,
232>;
233
234#[derive(Clone)]
235struct HandlerSlot {
236 cancelled: Arc<AtomicBool>,
237 options: EventOptions,
241 handler: Handler,
242}
243
244#[derive(Clone)]
245struct WaterfallSlot {
246 cancelled: Arc<AtomicBool>,
247 options: EventOptions,
251 handler: WaterfallHandler,
252}
253
254pub struct EventsService {
255 handlers: RwLock<HashMap<EventId, Vec<HandlerSlot>>>,
256 waterfall_handlers: RwLock<HashMap<EventId, Vec<WaterfallSlot>>>,
257 bus: tokio::sync::broadcast::Sender<(EventId, serde_json::Value)>,
258 dispatch_counts: Mutex<BTreeMap<String, u64>>,
260}
261
262impl EventsService {
263 pub fn new() -> Self {
264 let (tx, _rx) = tokio::sync::broadcast::channel(32);
265 let svc = Self {
266 handlers: RwLock::new(HashMap::new()),
267 waterfall_handlers: RwLock::new(HashMap::new()),
268 bus: tx,
269 dispatch_counts: Mutex::new(BTreeMap::new()),
270 };
271 svc.register_default_admit_handler();
272 svc
273 }
274
275 fn register_default_admit_handler(&self) {
276 let cancelled = Arc::new(AtomicBool::new(false));
277 let slot = HandlerSlot {
278 cancelled,
279 options: EventOptions::default(),
280 handler: Arc::new(|payload| Box::pin(async move { Ok(default_agent_admit(payload)) })),
281 };
282 self.handlers
283 .write()
284 .entry("agent.admit".into())
285 .or_default()
286 .push(slot);
287 }
288
289 pub fn listener_count(&self, event: &str) -> usize {
294 let flat = self
295 .handlers
296 .read()
297 .get(event)
298 .map(|slots| {
299 slots
300 .iter()
301 .filter(|slot| !slot.cancelled.load(Ordering::SeqCst))
302 .count()
303 })
304 .unwrap_or(0);
305 let waterfall = self
306 .waterfall_handlers
307 .read()
308 .get(event)
309 .map(|slots| {
310 slots
311 .iter()
312 .filter(|slot| !slot.cancelled.load(Ordering::SeqCst))
313 .count()
314 })
315 .unwrap_or(0);
316 flat + waterfall
317 }
318
319 pub fn dispatch_snapshot(&self) -> (u64, Vec<(String, u64)>) {
322 let map = self.dispatch_counts.lock();
323 let total = map.values().sum();
324 (total, map.iter().map(|(k, v)| (k.clone(), *v)).collect())
325 }
326
327 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<(EventId, serde_json::Value)> {
329 self.bus.subscribe()
330 }
331
332 pub fn once<F, Fut>(&self, event: EventId, handler: F) -> Box<dyn Disposable>
343 where
344 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
345 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
346 {
347 self.once_with(event, EventOptions::default(), handler)
348 }
349
350 pub fn once_with<F, Fut>(
358 &self,
359 event: EventId,
360 options: EventOptions,
361 handler: F,
362 ) -> Box<dyn Disposable>
363 where
364 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
365 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
366 {
367 debug_enforce_listener(&event, false);
368 if !blocking_listener_veto(self, &event) {
372 return Box::new(|| {});
373 }
374 let claim = Arc::new(AtomicBool::new(false));
381 let slot_flag = claim.clone();
382 let handle_flag = claim.clone();
383 let user = Arc::new(handler);
388 let once_handler: Handler = {
389 let user = user.clone();
390 Arc::new(move |payload: serde_json::Value| {
391 let claimed = claim.swap(true, Ordering::SeqCst);
392 let user = user.clone();
393 Box::pin(async move {
394 if claimed {
395 return Ok(payload);
397 }
398 user(payload).await
399 })
400 })
401 };
402 let slot = HandlerSlot {
403 cancelled: slot_flag,
404 options,
405 handler: once_handler,
406 };
407 self.insert_handler(event, options.prepend, slot);
408 Box::new(move || {
409 handle_flag.store(true, Ordering::SeqCst);
410 })
411 }
412
413 pub fn on<F, Fut>(&self, event: EventId, handler: F) -> Box<dyn Disposable>
414 where
415 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
416 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
417 {
418 self.on_with(event, EventOptions::default(), handler)
419 }
420
421 pub fn on_with<F, Fut>(
426 &self,
427 event: EventId,
428 options: EventOptions,
429 handler: F,
430 ) -> Box<dyn Disposable>
431 where
432 F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
433 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
434 {
435 debug_enforce_listener(&event, false);
436 if !blocking_listener_veto(self, &event) {
439 return Box::new(|| {});
440 }
441 let cancelled = Arc::new(AtomicBool::new(false));
442 let slot = HandlerSlot {
443 cancelled: cancelled.clone(),
444 options,
445 handler: Arc::new(move |v| Box::pin(handler(v))),
446 };
447 self.insert_handler(event, options.prepend, slot);
448 Box::new(move || {
449 cancelled.store(true, Ordering::SeqCst);
450 })
451 }
452
453 fn insert_handler(&self, event: EventId, prepend: bool, slot: HandlerSlot) {
456 let mut handlers = self.handlers.write();
457 let entry = handlers.entry(event).or_default();
458 if prepend {
459 entry.insert(0, slot);
460 } else {
461 entry.push(slot);
462 }
463 }
464
465 pub fn on_waterfall<F, Fut>(&self, event: EventId, handler: F) -> Box<dyn Disposable>
474 where
475 F: Fn(serde_json::Value, WaterfallNext) -> Fut + Send + Sync + 'static,
476 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
477 {
478 debug_enforce_listener(&event, true);
479 if !blocking_listener_veto(self, &event) {
480 return Box::new(|| {});
481 }
482 let cancelled = Arc::new(AtomicBool::new(false));
483 let slot = WaterfallSlot {
484 cancelled: cancelled.clone(),
485 options: EventOptions::default(),
486 handler: Arc::new(move |v, next| Box::pin(handler(v, next))),
487 };
488 let mut handlers = self.waterfall_handlers.write();
489 let entry = handlers.entry(event).or_default();
490 entry.push(slot);
491 Box::new(move || {
492 cancelled.store(true, Ordering::SeqCst);
493 })
494 }
495
496 fn active_handlers_filtered(
504 &self,
505 event: &EventId,
506 filter: Option<&ListenerFilter>,
507 ) -> Vec<Handler> {
508 let Some(filter) = filter else {
509 return self.active_handlers(event);
510 };
511 let mut handlers = self.handlers.write();
512 let active = {
513 let Some(slots) = handlers.get_mut(event) else {
514 return Vec::new();
515 };
516 slots.retain(|slot| !slot.cancelled.load(Ordering::SeqCst));
517 slots
518 .iter()
519 .filter(|slot| slot.options.global || filter(&slot.options))
520 .map(|slot| slot.handler.clone())
521 .collect::<Vec<_>>()
522 };
523 if active.is_empty() && handlers.get(event).is_some_and(Vec::is_empty) {
524 handlers.remove(event);
525 }
526 active
527 }
528
529 fn active_handlers(&self, event: &EventId) -> Vec<Handler> {
530 let mut handlers = self.handlers.write();
531 let active = {
532 let Some(slots) = handlers.get_mut(event) else {
533 return Vec::new();
534 };
535 slots.retain(|slot| !slot.cancelled.load(Ordering::SeqCst));
536 slots
537 .iter()
538 .map(|slot| slot.handler.clone())
539 .collect::<Vec<_>>()
540 };
541 if active.is_empty() {
542 handlers.remove(event);
543 }
544 active
545 }
546
547 fn active_waterfall(&self, event: &EventId) -> Vec<WaterfallHandler> {
548 self.active_waterfall_filtered(event, None)
549 }
550
551 fn active_waterfall_filtered(
555 &self,
556 event: &EventId,
557 filter: Option<&ListenerFilter>,
558 ) -> Vec<WaterfallHandler> {
559 let mut handlers = self.waterfall_handlers.write();
560 let active = {
561 let Some(slots) = handlers.get_mut(event) else {
562 return Vec::new();
563 };
564 slots.retain(|slot| !slot.cancelled.load(Ordering::SeqCst));
565 slots
566 .iter()
567 .filter(|slot| match filter {
568 None => true,
569 Some(filter) => slot.options.global || filter(&slot.options),
570 })
571 .map(|slot| slot.handler.clone())
572 .collect::<Vec<_>>()
573 };
574 if active.is_empty() && handlers.get(event).is_some_and(Vec::is_empty) {
578 handlers.remove(event);
579 }
580 active
581 }
582
583 fn observe_dispatch(&self, mode: Dispatch, name: &EventId, args: &serde_json::Value) {
589 if is_internal_meta_event(name) || self.listener_count(INTERNAL_DISPATCH_EVENT) == 0 {
590 return;
591 }
592 let Ok(payload) = serde_json::to_value(InternalDispatchPayload {
593 mode: mode.to_string(),
594 name: name.clone(),
595 args: args.clone(),
596 }) else {
597 return;
598 };
599 for handler in self.active_handlers(&INTERNAL_DISPATCH_EVENT.to_string()) {
600 let p = payload.clone();
601 tokio::spawn(async move {
602 let _ = handler(p).await;
603 });
604 }
605 }
606
607 pub fn emit_filtered(
616 &self,
617 event: EventId,
618 args: serde_json::Value,
619 filter: ListenerFilter,
620 ) -> Result<serde_json::Value, CordisError> {
621 debug_enforce_dispatch(&event, Dispatch::Emit);
622 *self
623 .dispatch_counts
624 .lock()
625 .entry(event.to_string())
626 .or_insert(0) += 1;
627 self.observe_dispatch(Dispatch::Emit, &event, &args);
628 let _ = self.bus.send((event.clone(), args.clone()));
629 for h in self.active_handlers_filtered(&event, Some(&filter)) {
630 let p = args.clone();
631 tokio::spawn(async move {
632 let _ = h(p).await;
633 });
634 }
635 Ok(serde_json::Value::Null)
636 }
637
638 pub async fn bail_from(
645 &self,
646 event: EventId,
647 payload: serde_json::Value,
648 filter: Option<ListenerFilter>,
649 ) -> Result<serde_json::Value, CordisError> {
650 debug_enforce_dispatch(&event, Dispatch::Bail);
651 *self
652 .dispatch_counts
653 .lock()
654 .entry(event.to_string())
655 .or_insert(0) += 1;
656 let handlers = match filter {
657 Some(filter) => self.active_handlers_filtered(&event, Some(&filter)),
658 None => self.active_handlers(&event),
659 };
660 run_bail_handlers(handlers, payload).await
661 }
662
663 pub async fn waterfall_from(
667 &self,
668 event: EventId,
669 payload: serde_json::Value,
670 filter: Option<ListenerFilter>,
671 ) -> Result<serde_json::Value, CordisError> {
672 debug_enforce_dispatch(&event, Dispatch::Waterfall);
673 *self
674 .dispatch_counts
675 .lock()
676 .entry(event.to_string())
677 .or_insert(0) += 1;
678 let handlers = self.active_waterfall_filtered(&event, filter.as_ref());
679 if handlers.is_empty() {
680 return Ok(payload);
681 }
682 run_waterfall_chain(handlers, 0, payload, None).await
683 }
684
685 pub async fn dispatch(
686 &self,
687 event: EventId,
688 payload: serde_json::Value,
689 mode: Dispatch,
690 ) -> Result<serde_json::Value, CordisError> {
691 debug_enforce_dispatch(&event, mode);
692 *self
693 .dispatch_counts
694 .lock()
695 .entry(event.to_string())
696 .or_insert(0) += 1;
697 self.observe_dispatch(mode, &event, &payload);
698 let handlers = self.active_handlers(&event);
699 match mode {
700 Dispatch::Waterfall => {
703 let wf_handlers = self.active_waterfall(&event);
704 if wf_handlers.is_empty() {
705 return Ok(payload);
706 }
707 run_waterfall_chain(wf_handlers, 0, payload, None).await
708 }
709 Dispatch::Emit => {
712 let _ = self.bus.send((event, payload.clone()));
713 for h in handlers {
714 let p = payload.clone();
715 tokio::spawn(async move {
716 let _ = h(p).await;
717 });
718 }
719 Ok(serde_json::Value::Null)
720 }
721 Dispatch::Parallel => {
725 let mut set = tokio::task::JoinSet::new();
726 for (name, h) in handlers.into_iter().enumerate() {
727 let p = payload.clone();
728 set.spawn(async move { (name, h(p).await) });
729 }
730 let mut failures: Vec<(String, String)> = Vec::new();
731 while let Some(res) = set.join_next().await {
732 match res {
733 Err(join_err) => {
734 let message = match join_err.try_into_panic() {
738 Ok(payload) => panic_payload_message(&payload),
739 Err(cancelled) => cancelled.to_string(),
740 };
741 failures.push(("listener-task".to_string(), message));
742 }
743 Ok((name, Err(err))) => {
744 failures.push((format!("listener[{name}]"), err.message()))
745 }
746 Ok((_, Ok(_))) => {}
747 }
748 }
749 if failures.is_empty() {
750 return Ok(serde_json::Value::Null);
751 }
752 tracing::warn!(
753 event = %event,
754 failures = %format_listener_errors(&failures),
755 "parallel dispatch collected listener failures"
756 );
757 Err(CordisError::Internal(format_listener_errors(&failures)))
758 }
759 Dispatch::Serial | Dispatch::Bail => run_bail_handlers(handlers, payload).await,
764 }
765 }
766
767 pub async fn waterfall_around<F, Fut>(
775 &self,
776 event: EventId,
777 payload: serde_json::Value,
778 core: F,
779 ) -> Result<serde_json::Value, CordisError>
780 where
781 F: FnOnce(serde_json::Value) -> Fut + Send + 'static,
782 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
783 {
784 debug_enforce_dispatch(&event, Dispatch::Waterfall);
785 self.observe_dispatch(Dispatch::Waterfall, &event, &payload);
786 let handlers = self.active_waterfall(&event);
787 if handlers.is_empty() {
788 return core(payload).await;
789 }
790 let core: WaterfallCore = Box::new(move |p| {
791 Box::pin(core(p))
792 as Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
793 });
794 run_waterfall_chain(handlers, 0, payload, Some(core)).await
795 }
796
797 pub async fn waterfall_async_from<F, Fut>(
802 &self,
803 event: EventId,
804 payload: serde_json::Value,
805 filter: Option<ListenerFilter>,
806 core: F,
807 ) -> Result<serde_json::Value, CordisError>
808 where
809 F: FnOnce(serde_json::Value) -> Fut + Send + 'static,
810 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
811 {
812 debug_enforce_dispatch(&event, Dispatch::Waterfall);
813 *self
814 .dispatch_counts
815 .lock()
816 .entry(event.to_string())
817 .or_insert(0) += 1;
818 let handlers = self.active_waterfall_filtered(&event, filter.as_ref());
819 if handlers.is_empty() {
820 return core(payload).await;
821 }
822 let core: WaterfallCore = Box::new(move |p| {
823 Box::pin(core(p))
824 as Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
825 });
826 run_waterfall_chain(handlers, 0, payload, Some(core)).await
827 }
828
829 pub async fn intercept_get(
835 &self,
836 service: &str,
837 ctx_hint: Option<String>,
838 ) -> Result<Option<serde_json::Value>, CordisError> {
839 if self.listener_count(INTERNAL_GET_EVENT) == 0 {
840 return Ok(None);
841 }
842 let payload = serde_json::json!({ "service": service, "ctx": ctx_hint });
843 let out = self
844 .bail_from(INTERNAL_GET_EVENT.into(), payload, None)
845 .await?;
846 Ok((!out.is_null()).then_some(out))
847 }
848
849 pub async fn intercept_set(
853 &self,
854 service: &str,
855 ctx_hint: Option<String>,
856 ) -> Result<(), CordisError> {
857 if self.listener_count(INTERNAL_SET_EVENT) == 0 {
858 return Ok(());
859 }
860 let payload = serde_json::json!({ "service": service, "ctx": ctx_hint });
861 self.bail_from(INTERNAL_SET_EVENT.into(), payload, None)
862 .await?;
863 Ok(())
864 }
865
866 pub async fn intercept_config(
871 &self,
872 raw: serde_json::Value,
873 ) -> Result<serde_json::Value, CordisError> {
874 if self.listener_count(INTERNAL_CONFIG_EVENT) == 0 {
875 return Ok(raw);
876 }
877 self.bail_from(INTERNAL_CONFIG_EVENT.into(), raw, None).await
878 }
879
880 pub async fn intercept_update(&self, service: &str) -> Result<bool, CordisError> {
885 if self.listener_count(INTERNAL_UPDATE_EVENT) == 0 {
886 return Ok(true);
887 }
888 let payload = serde_json::json!({ "service": service });
889 let out = self
890 .bail_from(INTERNAL_UPDATE_EVENT.into(), payload, None)
891 .await?;
892 Ok(!(out.is_null() || out.as_bool() == Some(false)))
893 }
894
895 pub async fn intercept_listener(&self, event: &str) -> Result<bool, CordisError> {
900 if self.listener_count(INTERNAL_LISTENER_EVENT) == 0 {
901 return Ok(true);
902 }
903 let payload = serde_json::json!({ "event": event });
904 let out = self
905 .bail_from(INTERNAL_LISTENER_EVENT.into(), payload, None)
906 .await?;
907 Ok(out.is_null() || out.as_bool() == Some(true))
908 }
909
910 pub async fn dispatch_typed<E: crate::events_payload::TypedEvent>(
915 &self,
916 payload: &E::Payload,
917 ) -> Result<serde_json::Value, CordisError> {
918 let value =
919 serde_json::to_value(payload).map_err(|e| CordisError::Configuration(e.to_string()))?;
920 self.dispatch(E::NAME.to_string(), value, E::MODE).await
921 }
922
923 pub fn on_typed<E, F, Fut>(&self, handler: F) -> Box<dyn Disposable>
931 where
932 E: crate::events_payload::TypedEvent,
933 F: Fn(E::Payload) -> Fut + Send + Sync + 'static,
934 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
935 {
936 debug_enforce_listener(&E::NAME.to_string(), E::AROUND);
937 let wrapped = move |v: serde_json::Value| {
938 let fut = match serde_json::from_value::<E::Payload>(v.clone()) {
939 Ok(payload) => handler(payload),
940 Err(err) => {
941 tracing::warn!(event = E::NAME, error = %err, "typed listener skipped malformed payload");
942 return Box::pin(async { Ok(v) })
943 as Pin<
944 Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>,
945 >;
946 }
947 };
948 Box::pin(fut)
949 as Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
950 };
951 self.on(E::NAME.to_string(), wrapped)
952 }
953
954 pub fn on_typed_waterfall<E, F, Fut>(&self, handler: F) -> Box<dyn Disposable>
960 where
961 E: crate::events_payload::TypedEvent,
962 F: Fn(E::Payload, WaterfallNext) -> Fut + Send + Sync + 'static,
963 Fut: Future<Output = Result<serde_json::Value, CordisError>> + Send + 'static,
964 {
965 debug_enforce_listener(&E::NAME.to_string(), E::AROUND);
966 let wrapped = move |v: serde_json::Value, next: WaterfallNext| {
967 let fut = match serde_json::from_value::<E::Payload>(v.clone()) {
968 Ok(payload) => handler(payload, next),
969 Err(err) => {
970 tracing::warn!(event = E::NAME, error = %err, "typed listener skipped malformed payload");
971 return Box::pin(async move {
972 next(v).await
975 })
976 as Pin<
977 Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>,
978 >;
979 }
980 };
981 Box::pin(fut)
982 as Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
983 };
984 self.on_waterfall(E::NAME.to_string(), wrapped)
985 }
986}
987
988impl Default for EventsService {
989 fn default() -> Self {
990 Self::new()
991 }
992}
993
994struct InterceptFence;
999
1000impl InterceptFence {
1001 fn enter() -> Option<Self> {
1002 INTERCEPT_FENCE.with(|fence| {
1003 if fence.get() {
1004 None
1005 } else {
1006 fence.set(true);
1007 Some(Self)
1008 }
1009 })
1010 }
1011}
1012
1013impl Drop for InterceptFence {
1014 fn drop(&mut self) {
1015 INTERCEPT_FENCE.with(|fence| fence.set(false));
1016 }
1017}
1018
1019thread_local! {
1020 static INTERCEPT_FENCE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1021}
1022
1023fn bridge_handle() -> Option<tokio::runtime::Handle> {
1028 let handle = tokio::runtime::Handle::try_current().ok()?;
1029 if handle.runtime_flavor() == tokio::runtime::RuntimeFlavor::MultiThread {
1030 Some(handle)
1031 } else {
1032 None
1033 }
1034}
1035
1036fn blocking_listener_veto(svc: &EventsService, event: &str) -> bool {
1045 if svc.listener_count(INTERNAL_LISTENER_EVENT) == 0 {
1046 return true;
1047 }
1048 let Some(_fence) = InterceptFence::enter() else {
1049 return true;
1050 };
1051 let Some(handle) = bridge_handle() else {
1052 tracing::warn!(
1053 event = %event,
1054 "internal/listener veto listener present but runtime cannot block in place; allowing registration"
1055 );
1056 return true;
1057 };
1058 let svc: &'static EventsService = unsafe { &*(svc as *const EventsService) };
1062 tokio::task::block_in_place(|| {
1063 handle.block_on(async move {
1064 svc.intercept_listener(event).await.unwrap_or(false)
1066 })
1067 })
1068}
1069
1070#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1072pub(crate) enum ReadVerdict {
1073 Pass,
1075 RedirectFrame,
1079 Refuse,
1081}
1082
1083pub(crate) fn blocking_intercept_get(events: &EventsService, service: &str) -> ReadVerdict {
1087 if events.listener_count(INTERNAL_GET_EVENT) == 0 {
1088 return ReadVerdict::Pass;
1089 }
1090 let Some(_fence) = InterceptFence::enter() else {
1091 return ReadVerdict::Pass;
1093 };
1094 let Some(handle) = bridge_handle() else {
1095 tracing::warn!(
1096 service,
1097 "internal/get listener present but runtime cannot block in place; passing read through"
1098 );
1099 return ReadVerdict::Pass;
1100 };
1101 let events: &'static EventsService = unsafe { &*(events as *const EventsService) };
1104 let service = service.to_string();
1105 tokio::task::block_in_place(|| {
1106 handle.block_on(async move {
1107 match events.intercept_get(&service, None).await {
1108 Err(_) => ReadVerdict::Refuse,
1109 Ok(None) => ReadVerdict::Pass,
1110 Ok(Some(out)) => {
1111 if out.is_null() {
1112 ReadVerdict::Pass
1113 } else if out.get("refuse").and_then(|v| v.as_bool()) == Some(true) {
1114 ReadVerdict::Refuse
1115 } else {
1116 ReadVerdict::RedirectFrame
1117 }
1118 }
1119 }
1120 })
1121 })
1122}
1123
1124pub(crate) fn blocking_intercept_set(events: &EventsService, service: &str) -> Result<(), CordisError> {
1127 if events.listener_count(INTERNAL_SET_EVENT) == 0 {
1128 return Ok(());
1129 }
1130 let Some(_fence) = InterceptFence::enter() else {
1131 return Ok(());
1132 };
1133 let Some(handle) = bridge_handle() else {
1134 tracing::warn!(
1135 service,
1136 "internal/set listener present but runtime cannot block in place; allowing write"
1137 );
1138 return Ok(());
1139 };
1140 let events: &'static EventsService = unsafe { &*(events as *const EventsService) };
1142 let service = service.to_string();
1143 tokio::task::block_in_place(|| {
1144 handle.block_on(async move { events.intercept_set(&service, None).await })
1145 })
1146}
1147
1148pub(crate) fn blocking_intercept_config(
1153 events: &EventsService,
1154 raw: serde_json::Value,
1155) -> Result<serde_json::Value, CordisError> {
1156 if events.listener_count(INTERNAL_CONFIG_EVENT) == 0 {
1157 return Ok(raw);
1158 }
1159 let Some(_fence) = InterceptFence::enter() else {
1160 return Ok(raw);
1161 };
1162 let Some(handle) = bridge_handle() else {
1163 tracing::warn!(
1164 "internal/config listener present but runtime cannot block in place; using raw config"
1165 );
1166 return Ok(raw);
1167 };
1168 let events: &'static EventsService = unsafe { &*(events as *const EventsService) };
1170 tokio::task::block_in_place(|| handle.block_on(events.intercept_config(raw)))
1171}
1172
1173fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> String {
1176 if let Some(s) = payload.downcast_ref::<&str>() {
1177 (*s).to_string()
1178 } else if let Some(s) = payload.downcast_ref::<String>() {
1179 s.clone()
1180 } else {
1181 "listener panicked".to_string()
1182 }
1183}
1184
1185fn json_u64(v: &serde_json::Value, key: &str) -> Option<u64> {
1186 v.get(key).and_then(|x| {
1187 x.as_u64()
1188 .or_else(|| x.as_i64().and_then(|n| u64::try_from(n).ok()))
1189 })
1190}
1191
1192fn default_agent_admit(payload: serde_json::Value) -> serde_json::Value {
1195 if payload.get("tier").and_then(|v| v.as_str()) == Some("enterprise") {
1196 return serde_json::Value::Null;
1197 }
1198 let monthly = json_u64(&payload, "monthly").unwrap_or(0);
1199 let daily = json_u64(&payload, "daily").unwrap_or(0);
1200 let Some(rpm) = json_u64(&payload, "requests_per_month") else {
1201 return serde_json::Value::Null;
1202 };
1203 let Some(rpd) = json_u64(&payload, "requests_per_day") else {
1204 return serde_json::Value::Null;
1205 };
1206 if monthly >= rpm {
1207 return serde_json::json!({ "deny": "monthly" });
1208 }
1209 if daily >= rpd {
1210 return serde_json::json!({ "deny": "daily" });
1211 }
1212 serde_json::Value::Null
1213}
1214
1215impl Service for EventsService {}
1216
1217async fn run_bail_handlers(
1218 handlers: Vec<Handler>,
1219 payload: serde_json::Value,
1220) -> Result<serde_json::Value, CordisError> {
1221 for handler in handlers {
1222 let result = handler(payload.clone()).await?;
1223 if !result.is_null() {
1224 return Ok(result);
1225 }
1226 }
1227 Ok(payload)
1228}
1229
1230type WaterfallCore = Box<
1233 dyn FnOnce(
1234 serde_json::Value,
1235 )
1236 -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
1237 + Send,
1238>;
1239
1240fn run_waterfall_chain(
1249 handlers: Vec<WaterfallHandler>,
1250 index: usize,
1251 payload: serde_json::Value,
1252 core: Option<WaterfallCore>,
1253) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>> {
1254 Box::pin(async move {
1255 if index >= handlers.len() {
1256 return match core {
1257 Some(core) => core(payload).await,
1258 None => Ok(payload),
1259 };
1260 }
1261 let handler = handlers[index].clone();
1262 let next = move |p: serde_json::Value| {
1266 let remaining = handlers.clone();
1267 Box::pin(async move { run_waterfall_chain(remaining, index + 1, p, core).await })
1268 as Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>>
1269 };
1270 handler(payload, Box::new(next)).await
1271 })
1272}
1273
1274#[cfg(test)]
1275mod tests {
1276 use super::*;
1277 use std::sync::atomic::AtomicUsize;
1278
1279 #[tokio::test]
1280 async fn on_dispose_unregisters_handler() {
1281 let svc = EventsService::new();
1282 let flag = Arc::new(AtomicBool::new(false));
1283 let f = flag.clone();
1284 let d = svc.on("gone".into(), move |_v| {
1285 let f = f.clone();
1286 async move {
1287 f.store(true, Ordering::SeqCst);
1288 Ok(serde_json::Value::Null)
1289 }
1290 });
1291 d.dispose();
1292 svc.dispatch("gone".into(), serde_json::json!({}), Dispatch::Emit)
1293 .await
1294 .unwrap();
1295 svc.dispatch("gone".into(), serde_json::json!({}), Dispatch::Serial)
1296 .await
1297 .unwrap();
1298 assert!(svc.handlers.read().get("gone").is_none());
1299 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
1300 assert!(
1301 !flag.load(Ordering::SeqCst),
1302 "disposed on() handler must not run for Emit or Serial"
1303 );
1304 }
1305
1306 #[tokio::test]
1307 async fn on_waterfall_dispose_unregisters_handler() {
1308 let svc = EventsService::new();
1309 let flag = Arc::new(AtomicBool::new(false));
1310 let f = flag.clone();
1311 let d = svc.on_waterfall("gone.wf".into(), move |payload, _next| {
1312 let f = f.clone();
1313 async move {
1314 f.store(true, Ordering::SeqCst);
1315 Ok(payload)
1316 }
1317 });
1318 d.dispose();
1319 svc.dispatch(
1320 "gone.wf".into(),
1321 serde_json::json!({ "n": 1 }),
1322 Dispatch::Waterfall,
1323 )
1324 .await
1325 .unwrap();
1326 assert!(
1327 !flag.load(Ordering::SeqCst),
1328 "disposed on_waterfall handler must not run"
1329 );
1330 assert!(svc.waterfall_handlers.read().get("gone.wf").is_none());
1331 }
1332
1333 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1337 async fn once_fires_exactly_once_concurrently() {
1338 let svc = std::sync::Arc::new(EventsService::new());
1339 let runs = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1340 let r = runs.clone();
1341 svc.once("once.test".into(), move |payload| {
1342 let r = r.clone();
1343 async move {
1344 r.fetch_add(1, Ordering::SeqCst);
1345 Ok(payload)
1346 }
1347 });
1348
1349 let mut tasks = tokio::task::JoinSet::new();
1352 for _ in 0..16 {
1353 let svc = std::sync::Arc::clone(&svc);
1354 tasks.spawn(async move {
1355 svc.dispatch(
1356 "once.test".into(),
1357 serde_json::json!({}),
1358 Dispatch::Parallel,
1359 )
1360 .await
1361 });
1362 }
1363 while let Some(res) = tasks.join_next().await {
1364 res.expect("dispatch task").expect("parallel dispatch ok");
1365 }
1366 assert_eq!(
1367 runs.load(Ordering::SeqCst),
1368 1,
1369 "exactly one concurrent dispatch may run the once handler"
1370 );
1371 }
1372
1373 #[tokio::test]
1377 async fn once_stays_registered_when_skipped_by_bail() {
1378 let svc = EventsService::new();
1379 let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1380 let first = ran.clone();
1381 let bailer = svc.on("once.bail".into(), move |_payload| {
1383 let first = first.clone();
1384 async move {
1385 first.fetch_add(1, Ordering::SeqCst);
1386 Ok(serde_json::json!({ "handled": true }))
1387 }
1388 });
1389 let second = ran.clone();
1390 svc.once("once.bail".into(), move |payload| {
1391 let second = second.clone();
1392 async move {
1393 second.fetch_add(1, Ordering::SeqCst);
1394 Ok(payload)
1395 }
1396 });
1397
1398 let out = svc
1401 .dispatch("once.bail".into(), serde_json::json!({}), Dispatch::Bail)
1402 .await
1403 .unwrap();
1404 assert_eq!(out, serde_json::json!({ "handled": true }));
1405 assert_eq!(ran.load(Ordering::SeqCst), 1, "only the bail handler ran");
1406 assert!(svc.handlers.read().get("once.bail").is_some());
1408
1409 let _ = svc
1412 .dispatch("once.bail".into(), serde_json::json!({}), Dispatch::Bail)
1413 .await;
1414 assert_eq!(
1415 ran.load(Ordering::SeqCst),
1416 2,
1417 "the bail handler claims every chain"
1418 );
1419 assert!(
1420 svc.handlers.read().get("once.bail").is_some(),
1421 "skipped-by-bail once slot stays registered"
1422 );
1423
1424 bailer.dispose();
1428 let out = svc
1429 .dispatch(
1430 "once.bail".into(),
1431 serde_json::json!({"n": 1}),
1432 Dispatch::Bail,
1433 )
1434 .await
1435 .unwrap();
1436 assert_eq!(out, serde_json::json!({"n": 1}));
1438 assert_eq!(
1439 ran.load(Ordering::SeqCst),
1440 3,
1441 "the surviving once slot fires exactly once"
1442 );
1443 let _ = svc
1446 .dispatch(
1447 "once.bail".into(),
1448 serde_json::json!({"n": 2}),
1449 Dispatch::Bail,
1450 )
1451 .await;
1452 assert_eq!(
1453 ran.load(Ordering::SeqCst),
1454 3,
1455 "spent once slot must not run again"
1456 );
1457 assert!(
1458 svc.handlers.read().get("once.bail").is_none(),
1459 "pruned from the registry after spending"
1460 );
1461 }
1462
1463 #[tokio::test]
1464 async fn parallel_returns_null_after_all_handlers_complete() {
1465 let svc = EventsService::new();
1466 let completed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1467 for value in [1, 2] {
1468 let completed = completed.clone();
1469 svc.on("parallel.result".into(), move |_payload| {
1470 let completed = completed.clone();
1471 async move {
1472 completed.fetch_add(value, Ordering::SeqCst);
1473 Ok(serde_json::json!({ "value": value }))
1474 }
1475 });
1476 }
1477
1478 let out = svc
1479 .dispatch(
1480 "parallel.result".into(),
1481 serde_json::json!({ "input": true }),
1482 Dispatch::Parallel,
1483 )
1484 .await
1485 .unwrap();
1486
1487 assert_eq!(out, serde_json::Value::Null);
1488 assert_eq!(completed.load(Ordering::SeqCst), 3);
1489 }
1490
1491 #[tokio::test]
1492 async fn serial_stops_at_first_non_null_result() {
1493 let svc = EventsService::new();
1494 let ran = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1495 let first = ran.clone();
1496 svc.on("serial.bail".into(), move |payload| {
1497 let first = first.clone();
1498 async move {
1499 first.fetch_add(1, Ordering::SeqCst);
1500 assert_eq!(payload["input"], true);
1501 Ok(serde_json::Value::Null)
1502 }
1503 });
1504 let second = ran.clone();
1505 svc.on("serial.bail".into(), move |payload| {
1506 let second = second.clone();
1507 async move {
1508 second.fetch_add(1, Ordering::SeqCst);
1509 assert_eq!(payload["input"], true);
1510 Ok(serde_json::json!({ "handled": true }))
1511 }
1512 });
1513 let third = ran.clone();
1514 svc.on("serial.bail".into(), move |_payload| {
1515 let third = third.clone();
1516 async move {
1517 third.fetch_add(1, Ordering::SeqCst);
1518 Ok(serde_json::json!({ "late": true }))
1519 }
1520 });
1521
1522 let out = svc
1523 .dispatch(
1524 "serial.bail".into(),
1525 serde_json::json!({ "input": true }),
1526 Dispatch::Serial,
1527 )
1528 .await
1529 .unwrap();
1530
1531 assert_eq!(out, serde_json::json!({ "handled": true }));
1532 assert_eq!(ran.load(Ordering::SeqCst), 2);
1533 }
1534
1535 #[tokio::test]
1536 async fn serial_preserves_original_payload_when_no_handler_bails() {
1537 let svc = EventsService::new();
1538 svc.on("serial.identity".into(), |_payload| async move {
1539 Ok(serde_json::Value::Null)
1540 });
1541 svc.on("serial.identity".into(), |_payload| async move {
1542 Ok(serde_json::Value::Null)
1543 });
1544
1545 let payload = serde_json::json!({ "input": [1, 2], "nested": { "ok": true } });
1546 let out = svc
1547 .dispatch("serial.identity".into(), payload.clone(), Dispatch::Serial)
1548 .await
1549 .unwrap();
1550
1551 assert_eq!(out, payload);
1552 }
1553
1554 #[tokio::test]
1555 async fn default_agent_admit_handler_denies_monthly() {
1556 let svc = EventsService::new();
1557 let out = svc
1558 .dispatch(
1559 "agent.admit".into(),
1560 serde_json::json!({
1561 "monthly": 10,
1562 "daily": 0,
1563 "requests_per_month": 10,
1564 "requests_per_day": 50,
1565 "tier": "free"
1566 }),
1567 Dispatch::Bail,
1568 )
1569 .await
1570 .unwrap();
1571 assert_eq!(out["deny"], "monthly");
1572 }
1573
1574 #[tokio::test]
1575 async fn default_agent_admit_handler_allows_under_quota() {
1576 let svc = EventsService::new();
1577 let out = svc
1578 .dispatch(
1579 "agent.admit".into(),
1580 serde_json::json!({
1581 "monthly": 0,
1582 "daily": 0,
1583 "requests_per_month": 10,
1584 "requests_per_day": 50,
1585 "tier": "free"
1586 }),
1587 Dispatch::Bail,
1588 )
1589 .await
1590 .unwrap();
1591 assert!(
1592 out.get("deny").is_none(),
1593 "under-quota must not deny, got {out}"
1594 );
1595 }
1596
1597 #[tokio::test]
1598 async fn waterfall_around_no_handlers_runs_core() {
1599 let svc = EventsService::new();
1600 let out = svc
1601 .waterfall_around(
1602 "around.empty".into(),
1603 serde_json::json!({}),
1604 |mut payload| async move {
1605 if let Some(obj) = payload.as_object_mut() {
1606 obj.insert("core".into(), serde_json::json!(true));
1607 }
1608 Ok(payload)
1609 },
1610 )
1611 .await
1612 .unwrap();
1613 assert_eq!(out["core"], true);
1614 }
1615
1616 #[tokio::test]
1617 async fn waterfall_around_handler_calls_next_then_core() {
1618 let svc = EventsService::new();
1619 svc.on_waterfall("around.wrap".into(), |mut payload, next| async move {
1620 if let Some(obj) = payload.as_object_mut() {
1621 obj.insert("wrap".into(), serde_json::json!(true));
1622 }
1623 next(payload).await
1624 });
1625 let out = svc
1626 .waterfall_around(
1627 "around.wrap".into(),
1628 serde_json::json!({}),
1629 |mut payload| async move {
1630 if let Some(obj) = payload.as_object_mut() {
1631 obj.insert("core".into(), serde_json::json!(true));
1632 }
1633 Ok(payload)
1634 },
1635 )
1636 .await
1637 .unwrap();
1638 assert_eq!(out["wrap"], true);
1639 assert_eq!(out["core"], true);
1640 }
1641
1642 #[tokio::test]
1643 async fn waterfall_around_short_circuit_skips_core() {
1644 let svc = EventsService::new();
1645 let flag = Arc::new(AtomicBool::new(false));
1646 svc.on_waterfall("around.short".into(), |payload, _next| async move {
1647 Ok(payload)
1648 });
1649 let f = flag.clone();
1650 let out = svc
1651 .waterfall_around(
1652 "around.short".into(),
1653 serde_json::json!({ "ok": true }),
1654 move |payload| {
1655 let f = f.clone();
1656 async move {
1657 f.store(true, Ordering::SeqCst);
1658 Ok(payload)
1659 }
1660 },
1661 )
1662 .await
1663 .unwrap();
1664 assert_eq!(out["ok"], true);
1665 assert!(
1666 !flag.load(Ordering::SeqCst),
1667 "core must not run when handler short-circuits"
1668 );
1669 }
1670
1671 #[tokio::test]
1676 async fn typed_dispatch_and_listener_round_trip() {
1677 let svc = EventsService::new();
1678 let seen = Arc::new(parking_lot::Mutex::new(
1679 None::<crate::events_payload::AgentUsagePayload>,
1680 ));
1681 let slot = seen.clone();
1682 let _d = svc.on_typed::<crate::events_payload::AgentUsageEvent, _, _>(move |p| {
1683 let slot = slot.clone();
1684 async move {
1685 *slot.lock() = Some(p);
1686 Ok(serde_json::Value::Null)
1687 }
1688 });
1689 svc.dispatch_typed::<crate::events_payload::AgentUsageEvent>(
1690 &crate::events_payload::AgentUsagePayload {
1691 tenant: Some("acme".into()),
1692 prompt: 3,
1693 completion: 4,
1694 total: 7,
1695 },
1696 )
1697 .await
1698 .unwrap();
1699 for _ in 0..100 {
1701 if seen.lock().is_some() {
1702 break;
1703 }
1704 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1705 }
1706 let got = seen.lock().clone().expect("handler must observe payload");
1707 assert_eq!(got.tenant.as_deref(), Some("acme"));
1708 assert_eq!(got.total, 7);
1709 }
1710
1711 #[tokio::test]
1714 async fn typed_listener_skips_malformed_payload_passthrough() {
1715 let svc = EventsService::new();
1716 let ran = Arc::new(AtomicBool::new(false));
1717 let flag = ran.clone();
1718 let _d = svc.on_typed::<crate::events_payload::AgentAdmitEvent, _, _>(move |_p| {
1719 let flag = flag.clone();
1720 async move {
1721 flag.store(true, Ordering::SeqCst);
1722 Ok(serde_json::json!({ "deny": "daily" }))
1723 }
1724 });
1725 let out = svc
1726 .dispatch(
1727 crate::events_catalog::ev::AGENT_ADMIT.into(),
1728 serde_json::json!({ "tenant_id": 42 }), Dispatch::Bail,
1730 )
1731 .await
1732 .unwrap();
1733 assert!(
1734 !ran.load(Ordering::SeqCst),
1735 "malformed payload must skip the typed handler"
1736 );
1737 assert_eq!(out["tenant_id"], 42, "value must pass through unchanged");
1738 }
1739
1740 #[tokio::test]
1743 async fn typed_waterfall_short_circuit() {
1744 use crate::events::WaterfallNext;
1745 let svc = EventsService::new();
1746 let _d = svc.on_typed_waterfall::<crate::events_payload::LlmGetClientEvent, _, _>(
1747 |p, next: WaterfallNext| async move {
1748 if p.capability == "blocked" {
1749 return Ok(serde_json::json!({ "deny": true }));
1750 }
1751 next(serde_json::json!({ "capability": p.capability })).await
1752 },
1753 );
1754 let denied = svc
1755 .dispatch(
1756 crate::events_catalog::ev::LLM_GET_CLIENT.into(),
1757 serde_json::json!({ "capability": "blocked" }),
1758 Dispatch::Waterfall,
1759 )
1760 .await
1761 .unwrap();
1762 assert_eq!(denied["deny"], true);
1763 let passed = svc
1764 .dispatch(
1765 crate::events_catalog::ev::LLM_GET_CLIENT.into(),
1766 serde_json::json!({ "capability": "chat" }),
1767 Dispatch::Waterfall,
1768 )
1769 .await
1770 .unwrap();
1771 assert_eq!(passed["capability"], "chat");
1772 }
1773
1774 #[test]
1775 fn summarize_listener_errors_formats_multiple_failures() {
1776 let summary = crate::events::summarize_listener_errors(vec![
1777 ("quota".to_string(), "monthly cap reached".to_string()),
1778 ("audit".to_string(), "db write failed".to_string()),
1779 ]);
1780 assert_eq!(
1781 summary,
1782 "2 listener failures: quota: monthly cap reached; audit: db write failed"
1783 );
1784 }
1785
1786 #[test]
1787 fn summarize_listener_errors_formats_single_failure() {
1788 let summary = crate::events::summarize_listener_errors(vec![(
1789 "solo".to_string(),
1790 "boom".to_string(),
1791 )]);
1792 assert_eq!(summary, "1 listener failure: solo: boom");
1793 assert_eq!(
1794 crate::events::summarize_listener_errors(Vec::new()),
1795 "0 listener failures"
1796 );
1797 }
1798
1799 #[test]
1800 fn aggregate_error_display_and_error_impl() {
1801 let agg = AggregateError {
1802 errors: vec![
1803 ("a".to_string(), "x".to_string()),
1804 ("b".to_string(), "y".to_string()),
1805 ],
1806 };
1807 assert_eq!(agg.to_string(), "2 listener failures: a: x; b: y");
1808 let dyn_err: &dyn std::error::Error = &agg;
1810 assert!(dyn_err.to_string().contains("b: y"));
1811 }
1812
1813 #[tokio::test]
1818 async fn parallel_dispatch_aggregates_all_listener_failures() {
1819 use crate::CordisError as Err;
1820 let svc = EventsService::new();
1821 let completed = Arc::new(AtomicBool::new(false));
1822 let c = completed.clone();
1823 svc.on("par.agg".into(), move |_p| {
1824 let c = c.clone();
1825 async move {
1826 c.store(true, Ordering::SeqCst);
1827 Ok(serde_json::json!({ "ok": true }))
1828 }
1829 });
1830 svc.on("par.agg".into(), |_p| async move {
1831 Err::<serde_json::Value, _>(Err::Configuration("first failure".into()))
1832 });
1833 svc.on("par.agg".into(), |_p| async move {
1834 Err::<serde_json::Value, _>(Err::Fiber("second failure".into()))
1835 });
1836
1837 let err = svc
1838 .dispatch("par.agg".into(), serde_json::json!({}), Dispatch::Parallel)
1839 .await
1840 .unwrap_err();
1841 let text = err.message();
1842 assert!(
1843 text.starts_with("internal kernel error: 2 listener failures:")
1844 && text.contains("listener[1]: configuration error: first failure")
1845 && text.contains("listener[2]: fiber error: second failure"),
1846 "aggregate must list every failing listener, got: {text}"
1847 );
1848 assert!(
1849 completed.load(Ordering::SeqCst),
1850 "healthy listener still ran"
1851 );
1852 }
1853
1854 #[tokio::test]
1857 async fn parallel_dispatch_single_failure_uses_singular_summary() {
1858 let svc = EventsService::new();
1859 svc.on("par.solo".into(), |_p| async move {
1860 Err::<serde_json::Value, _>(crate::CordisError::Internal("only one".into()))
1861 });
1862 let err = svc
1863 .dispatch("par.solo".into(), serde_json::json!({}), Dispatch::Parallel)
1864 .await
1865 .unwrap_err();
1866 assert_eq!(
1867 err.message(),
1868 "internal kernel error: 1 listener failure: listener[0]: internal kernel error: only one"
1869 );
1870 }
1871
1872 #[tokio::test]
1880 async fn prepend_ordering_observed() {
1881 let svc = EventsService::new();
1882 let order = Arc::new(parking_lot::Mutex::<Vec<String>>::new(Vec::new()));
1883
1884 for name in ["first", "second"] {
1885 let slot = order.clone();
1886 svc.on("prepend.test".into(), move |_p| {
1887 let slot = slot.clone();
1888 async move {
1889 slot.lock().push(name.to_string());
1890 Ok(serde_json::Value::Null)
1891 }
1892 });
1893 }
1894
1895 let prepended_slot = order.clone();
1897 svc.on_with(
1898 "prepend.test".into(),
1899 EventOptions {
1900 prepend: true,
1901 global: false,
1902 },
1903 move |_p| {
1904 let prepended_slot = prepended_slot.clone();
1905 async move {
1906 prepended_slot.lock().push("prepended".to_string());
1907 Ok(serde_json::Value::Null)
1908 }
1909 },
1910 );
1911
1912 let once_slot = order.clone();
1914 svc.once_with(
1915 "prepend.test".into(),
1916 EventOptions {
1917 prepend: true,
1918 global: false,
1919 },
1920 move |_p| {
1921 let once_slot = once_slot.clone();
1922 async move {
1923 once_slot.lock().push("once-prepended".to_string());
1924 Ok(serde_json::Value::Null)
1925 }
1926 },
1927 );
1928
1929 svc.dispatch(
1930 "prepend.test".into(),
1931 serde_json::json!({}),
1932 Dispatch::Serial,
1933 )
1934 .await
1935 .unwrap();
1936 assert_eq!(
1937 *order.lock(),
1938 ["once-prepended", "prepended", "first", "second"],
1939 "prepend inserts at the dispatch-order front; defaults append"
1940 );
1941
1942 svc.dispatch(
1945 "prepend.test".into(),
1946 serde_json::json!({}),
1947 Dispatch::Serial,
1948 )
1949 .await
1950 .unwrap();
1951 assert_eq!(
1952 order.lock()[4..],
1953 ["prepended", "first", "second"],
1954 "spent once slot drops out; relative order is stable"
1955 );
1956 }
1957
1958 #[tokio::test]
1962 async fn filter_excludes_nonmatching_contexts() {
1963 let svc = EventsService::new();
1964 let ran_a = Arc::new(AtomicUsize::new(0));
1967 let ran_b = Arc::new(AtomicUsize::new(0));
1968
1969 let a = ran_a.clone();
1970 svc.on_with("filtered.test".into(), EventOptions::default(), move |p| {
1971 let a = a.clone();
1972 async move {
1973 a.fetch_add(1, Ordering::SeqCst);
1974 Ok(p)
1975 }
1976 });
1977 let b = ran_b.clone();
1978 svc.on_with("filtered.test".into(), EventOptions::default(), move |p| {
1979 let b = b.clone();
1980 async move {
1981 b.fetch_add(1, Ordering::SeqCst);
1982 Ok(p)
1983 }
1984 });
1985
1986 svc.emit_filtered(
1988 "filtered.test".into(),
1989 serde_json::json!({ "tenant": "a" }),
1990 Box::new(|_opts| false),
1991 )
1992 .unwrap();
1993 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
1994 assert_eq!(ran_a.load(Ordering::SeqCst), 0, "rejecting filter excludes a");
1995 assert_eq!(ran_b.load(Ordering::SeqCst), 0, "rejecting filter excludes b");
1996
1997 svc.dispatch("filtered.test".into(), serde_json::json!({}), Dispatch::Emit)
1999 .await
2000 .unwrap();
2001 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
2002 assert_eq!(
2003 ran_a.load(Ordering::SeqCst),
2004 1,
2005 "listener a must be back for unfiltered dispatches"
2006 );
2007 assert_eq!(
2008 ran_b.load(Ordering::SeqCst),
2009 1,
2010 "listener b must be back for unfiltered dispatches"
2011 );
2012
2013 let handlers = svc.handlers.read();
2015 let slots = handlers.get("filtered.test").expect("entry kept");
2016 assert_eq!(
2017 slots.len(),
2018 2,
2019 "filter exclusion must not unregister anyone"
2020 );
2021 }
2022
2023 #[tokio::test]
2027 async fn global_bypasses_filter() {
2028 let svc = EventsService::new();
2029 let ran = Arc::new(AtomicUsize::new(0));
2030
2031 let b = ran.clone();
2033 svc.on_with(
2034 "global.test".into(),
2035 EventOptions::default(),
2036 move |payload| {
2037 let b = b.clone();
2038 async move {
2039 if payload["tenant"] == "b" {
2040 b.fetch_add(1, Ordering::SeqCst);
2041 }
2042 Ok(payload)
2043 }
2044 },
2045 );
2046
2047 let g = ran.clone();
2049 svc.on_with(
2050 "global.test".into(),
2051 EventOptions {
2052 prepend: false,
2053 global: true,
2054 },
2055 move |payload| {
2056 let g = g.clone();
2057 async move {
2058 if payload["tenant"] == "b" {
2059 g.fetch_add(10, Ordering::SeqCst);
2060 }
2061 Ok(payload)
2062 }
2063 },
2064 );
2065
2066 svc.emit_filtered(
2069 "global.test".into(),
2070 serde_json::json!({ "tenant": "b" }),
2071 Box::new(|_opts| false),
2072 )
2073 .unwrap();
2074 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
2075 assert_eq!(
2076 ran.load(Ordering::SeqCst),
2077 10,
2078 "global listener runs despite a rejecting filter; non-global does not"
2079 );
2080 }
2081
2082 #[tokio::test]
2090 async fn get_interceptor_rewrites_read() {
2091 let svc = EventsService::new();
2092 assert_eq!(svc.intercept_get("Svc", None).await.unwrap(), None);
2094
2095 let d = svc.on(INTERNAL_GET_EVENT.into(), |_payload| async move {
2096 Ok(serde_json::json!({ "service": "Svc", "rewritten": true }))
2097 });
2098 let out = svc.intercept_get("Svc", Some("tenant-a".into())).await;
2099 match out {
2100 Ok(Some(value)) => {
2101 assert_eq!(value["rewritten"], serde_json::json!(true));
2102 assert_eq!(value["service"], "Svc");
2103 }
2104 other => panic!("expected rewritten read, got {other:?}"),
2105 }
2106 d.dispose();
2108 assert_eq!(svc.intercept_get("Svc", None).await.unwrap(), None);
2109 }
2110
2111 #[tokio::test]
2114 async fn set_interceptor_vetoes_write_leaves_old_value() {
2115 let svc = EventsService::new();
2116 assert!(svc.intercept_set("Svc", None).await.is_ok());
2117
2118 let d = svc.on(INTERNAL_SET_EVENT.into(), |_payload| async move {
2119 Err::<serde_json::Value, CordisError>(CordisError::Configuration(
2120 "writes are frozen".into(),
2121 ))
2122 });
2123 let err = svc.intercept_set("Svc", None).await.unwrap_err();
2124 assert!(
2125 err.to_string().contains("frozen"),
2126 "veto error must surface, got {err}"
2127 );
2128 d.dispose();
2129 assert!(svc.intercept_set("Svc", None).await.is_ok());
2130 }
2131
2132 #[tokio::test]
2134 async fn config_interceptor_rewrites_effective_config() {
2135 let svc = EventsService::new();
2136 let raw = serde_json::json!({ "model": "base" });
2137 assert_eq!(svc.intercept_config(raw.clone()).await.unwrap(), raw);
2139
2140 let d = svc.on(INTERNAL_CONFIG_EVENT.into(), |raw| async move {
2141 let mut effective = raw;
2142 if let Some(obj) = effective.as_object_mut() {
2143 obj.insert("model".into(), serde_json::json!("rewritten"));
2144 obj.insert("seen_by_interceptor".into(), serde_json::json!(true));
2145 }
2146 Ok(effective)
2147 });
2148 let effective = svc.intercept_config(raw).await.unwrap();
2149 assert_eq!(effective["model"], "rewritten");
2150 assert_eq!(effective["seen_by_interceptor"], true);
2151 d.dispose();
2152
2153 svc.on(INTERNAL_CONFIG_EVENT.into(), |_raw| async move {
2155 Ok(serde_json::Value::Null)
2156 });
2157 let raw2 = serde_json::json!({ "keep": 1 });
2158 assert_eq!(svc.intercept_config(raw2.clone()).await.unwrap(), raw2);
2159 }
2160
2161 #[tokio::test]
2164 async fn update_interceptor_veto_skips_restart_keeps_config() {
2165 use crate::{Context, Fiber};
2166 let ctx = Context::new_root();
2167 let events = Arc::new(EventsService::new());
2168 ctx.provide_arc(events.clone());
2169 let fiber = Arc::new(Fiber::new());
2170 fiber.set_reload_context(&ctx);
2171 fiber.set_id(70_100);
2172
2173 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2174 let c = calls.clone();
2175 fiber.set_reload_runner(Box::new(move |_| {
2176 c.fetch_add(1, Ordering::SeqCst);
2177 Ok(true)
2178 }));
2179 fiber.declare_inject::<crate::ReflectService>();
2180 let _prov = ctx.provide(crate::ReflectService::new());
2181 fiber.refresh(&ctx).await;
2182 assert!(matches!(fiber.state(), crate::FiberState::Active { .. }));
2183 assert_eq!(calls.load(Ordering::SeqCst), 1, "initial apply ran");
2184
2185 let d = events.on(INTERNAL_UPDATE_EVENT.into(), |_payload| async move {
2187 Ok(serde_json::json!({ "veto": "maintenance window" }))
2188 });
2189 fiber.update(&ctx).await.unwrap();
2190 assert!(
2191 matches!(fiber.state(), crate::FiberState::Active { .. }),
2192 "vetoed update must keep the fiber Active, got {:?}",
2193 fiber.state()
2194 );
2195 assert_eq!(
2196 calls.load(Ordering::SeqCst),
2197 1,
2198 "runner must not run again under veto"
2199 );
2200 d.dispose();
2201
2202 fiber.update(&ctx).await.unwrap();
2205 assert!(matches!(fiber.state(), crate::FiberState::Active { .. }));
2206 }
2207
2208 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2212 async fn listener_interceptor_bail_cancels_registration_inert_handle() {
2213 let svc = EventsService::new();
2214 let allow_gate = svc.on(INTERNAL_LISTENER_EVENT.into(), |payload| async move {
2217 if payload["event"] == "blocked.event" {
2218 Ok(serde_json::json!("denied"))
2219 } else {
2220 Ok(serde_json::Value::Null)
2221 }
2222 });
2223
2224 let ran = Arc::new(AtomicBool::new(false));
2226 let flag = ran.clone();
2227 let handle = svc.on("blocked.event".into(), move |_p| {
2228 let flag = flag.clone();
2229 async move {
2230 flag.store(true, Ordering::SeqCst);
2231 Ok(serde_json::Value::Null)
2232 }
2233 });
2234 handle.dispose(); svc.dispatch("blocked.event".into(), serde_json::json!({}), Dispatch::Bail)
2236 .await
2237 .unwrap();
2238 tokio::time::sleep(std::time::Duration::from_millis(30)).await;
2239 assert!(
2240 !ran.load(Ordering::SeqCst),
2241 "cancelled registration must never run"
2242 );
2243
2244 let wf_handle = svc.on_waterfall(
2246 "blocked.event".into(),
2247 |_p: serde_json::Value, next| async move { next(_p).await },
2248 );
2249 wf_handle.dispose();
2250 assert_eq!(
2251 svc.listener_count("blocked.event"),
2252 0,
2253 "neither registry may hold a cancelled registration"
2254 );
2255
2256 let allowed = svc.on("allowed.event".into(), |_p| async move {
2259 Ok(serde_json::Value::Null)
2260 });
2261 allowed.dispose();
2262 assert_eq!(svc.listener_count("allowed.event"), 0, "dispose works");
2263
2264 let fail_gate = svc.on(INTERNAL_LISTENER_EVENT.into(), |_payload| async move {
2266 Err::<serde_json::Value, CordisError>(CordisError::Configuration("gate down".into()))
2267 });
2268 let second = svc.on("another.event".into(), |_p| async move {
2269 Ok(serde_json::Value::Null)
2270 });
2271 second.dispose();
2272 assert_eq!(
2273 svc.listener_count("another.event"),
2274 0,
2275 "erroring veto chain must fail closed"
2276 );
2277 fail_gate.dispose();
2280 allow_gate.dispose();
2281 }
2282
2283 fn p_owned(p: serde_json::Value) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, CordisError>> + Send>> {
2284 Box::pin(async move { Ok(p) })
2285 }
2286
2287 #[tokio::test]
2291 async fn internal_dispatch_observes_non_internal_only() {
2292 let svc = EventsService::new();
2293 let seen = Arc::new(Mutex::new(Vec::<InternalDispatchPayload>::new()));
2294 let s = seen.clone();
2295 svc.on(INTERNAL_DISPATCH_EVENT.into(), move |payload| {
2296 let s = s.clone();
2297 async move {
2298 if let Ok(parsed) =
2299 serde_json::from_value::<InternalDispatchPayload>(payload)
2300 {
2301 s.lock().push(parsed);
2302 }
2303 Ok(serde_json::Value::Null)
2304 }
2305 });
2306
2307 svc.dispatch("observed.a".into(), serde_json::json!({ "n": 1 }), Dispatch::Emit)
2309 .await
2310 .unwrap();
2311 svc.dispatch(
2312 "observed.b".into(),
2313 serde_json::json!({ "n": 2 }),
2314 Dispatch::Waterfall,
2315 )
2316 .await
2317 .unwrap();
2318 svc.dispatch("observed.c".into(), serde_json::json!({}), Dispatch::Bail)
2319 .await
2320 .unwrap();
2321 svc.on(INTERNAL_GET_EVENT.into(), |_p| async move {
2323 Ok(serde_json::json!({ "x": true }))
2324 });
2325 svc.intercept_get("SomeSvc", None).await.unwrap();
2326 for _ in 0..100 {
2328 if seen.lock().len() >= 3 {
2329 break;
2330 }
2331 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
2332 }
2333 let observed = seen.lock().clone();
2334 assert_eq!(
2335 observed.len(),
2336 3,
2337 "meta dispatch must NOT be observed; got {observed:?}"
2338 );
2339 assert_eq!(observed[0].mode, "emit");
2340 assert_eq!(observed[0].name, "observed.a");
2341 assert_eq!(observed[1].mode, "waterfall");
2342 assert_eq!(observed[1].name, "observed.b");
2343 assert_eq!(observed[2].mode, "bail");
2344 assert_eq!(observed[0].args["n"], 1);
2345 }
2346
2347 #[tokio::test]
2351 async fn interceptor_error_fails_fiber_activation() {
2352 use crate::{Context, Fiber};
2353 let ctx = Context::new_root();
2354 let events = Arc::new(EventsService::new());
2355 ctx.provide_arc(events.clone());
2356 events.on(INTERNAL_CONFIG_EVENT.into(), |_raw| async move {
2357 Err::<serde_json::Value, CordisError>(CordisError::Configuration(
2358 "config rejected by policy".into(),
2359 ))
2360 });
2361
2362 let fiber = Arc::new(Fiber::new());
2363 fiber.set_reload_context(&ctx);
2364 fiber.set_id(70_101);
2365 fiber.set_raw_config(serde_json::json!({ "model": "base" }));
2366 fiber.set_reload_runner(Box::new(|_| {
2367 panic!("runner must never run when config interception refuses");
2368 }));
2369 fiber.declare_inject::<crate::ReflectService>();
2370 let _prov = ctx.provide(crate::ReflectService::new());
2371
2372 fiber.refresh(&ctx).await;
2373 match fiber.state() {
2374 crate::FiberState::Failed { error } => {
2375 let msg = error.unwrap_or_default();
2376 assert!(
2377 msg.contains("config rejected by policy"),
2378 "failure must carry the interception error, got: {msg}"
2379 );
2380 }
2381 other => panic!("expected Failed activation, got {other:?}"),
2382 }
2383 }
2384
2385 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
2389 async fn target_carrying_dispatches_filter_per_dispatch() {
2390 let svc = EventsService::new();
2391 let flat_ran = Arc::new(AtomicUsize::new(0));
2392 let f = flat_ran.clone();
2393 svc.on_with(
2394 "wf.filtered".into(),
2395 EventOptions::default(),
2396 move |_p| {
2397 let f = f.clone();
2398 async move {
2399 f.fetch_add(1, Ordering::SeqCst);
2400 Ok(serde_json::Value::Null)
2401 }
2402 },
2403 );
2404 let wf_ran = Arc::new(AtomicUsize::new(0));
2405 let w = wf_ran.clone();
2406 svc.on_waterfall("wf.filtered".into(), move |_p, next| {
2407 let w = w.clone();
2408 async move {
2409 w.fetch_add(1, Ordering::SeqCst);
2410 next(_p).await
2411 }
2412 });
2413
2414 let out = svc
2416 .bail_from(
2417 "wf.filtered".into(),
2418 serde_json::json!({"n": 1}),
2419 Some(Box::new(|_opts| false)),
2420 )
2421 .await
2422 .unwrap();
2423 assert_eq!(out, serde_json::json!({"n": 1}));
2425 assert_eq!(
2426 flat_ran.load(Ordering::SeqCst),
2427 0,
2428 "rejecting filter must exclude the flat listener"
2429 );
2430
2431 let out = svc
2433 .waterfall_from(
2434 "wf.filtered".into(),
2435 serde_json::json!({"n": 1}),
2436 Some(Box::new(|_opts| false)),
2437 )
2438 .await
2439 .unwrap();
2440 assert_eq!(out, serde_json::json!({"n": 1}));
2441 assert_eq!(
2442 wf_ran.load(Ordering::SeqCst),
2443 0,
2444 "rejecting filter must exclude the waterfall listener"
2445 );
2446
2447 svc.bail_from(
2449 "wf.filtered".into(),
2450 serde_json::json!({"n": 2}),
2451 Some(Box::new(|_opts| true)),
2452 )
2453 .await
2454 .unwrap();
2455 assert_eq!(flat_ran.load(Ordering::SeqCst), 1);
2456 svc.waterfall_async_from(
2457 "wf.filtered".into(),
2458 serde_json::json!({"n": 2}),
2459 Some(Box::new(|_opts| true)),
2460 |mut p| async move {
2461 if let Some(obj) = p.as_object_mut() {
2462 obj.insert("core".into(), serde_json::json!(true));
2463 }
2464 Ok(p)
2465 },
2466 )
2467 .await
2468 .unwrap();
2469 assert_eq!(wf_ran.load(Ordering::SeqCst), 1);
2470
2471 assert_eq!(svc.listener_count("wf.filtered"), 2);
2473 }
2474}