1#![allow(missing_docs)]
2#![allow(dead_code)]
3
4use parking_lot::RwLock;
5use std::any::TypeId;
6use std::collections::{HashMap, HashSet, VecDeque};
7use std::sync::{Arc, Weak};
8use tokio::sync::watch;
9
10#[cfg(feature = "inventory")]
12pub struct CordisInventory {
13 pub name: &'static str,
14}
15
16#[cfg(feature = "inventory")]
17inventory::collect!(CordisInventory);
18
19#[cfg(feature = "inventory")]
20inventory::submit! {
21 CordisInventory { name: "RegistryService" }
22}
23
24#[cfg(feature = "inventory")]
25inventory::submit! {
26 CordisInventory { name: "EventsService" }
27}
28
29#[cfg(feature = "inventory")]
30inventory::submit! {
31 CordisInventory { name: "ReflectService" }
32}
33
34#[cfg(feature = "inventory")]
35inventory::submit! {
36 CordisInventory { name: "Loader" }
37}
38
39#[cfg(feature = "inventory")]
41inventory::submit! {
42 CordisPluginFactory { name: "EventsService", make: factory_events_service }
43}
44#[cfg(all(feature = "inventory", feature = "rhai"))]
45inventory::submit! {
46 CordisPluginFactory { name: "RhaiPolicy", make: factory_rhai_policy }
47}
48
49#[cfg(feature = "inventory")]
54pub type PluginFactoryFn = fn(&Arc<Context>, &serde_json::Value) -> Result<FiberId, CordisError>;
55
56#[cfg(feature = "inventory")]
58pub struct CordisPluginFactory {
59 pub name: &'static str,
61 pub make: PluginFactoryFn,
63}
64
65#[cfg(feature = "inventory")]
66inventory::collect!(CordisPluginFactory);
67
68#[cfg(feature = "inventory")]
74pub fn register_inventory_factories(reg: &PluginRegistry) {
75 for entry in inventory::iter::<CordisPluginFactory> {
76 reg.register(entry.name, Arc::new(entry.make));
77 }
78}
79
80#[cfg(feature = "inventory")]
81pub fn inventory_len() -> usize {
82 inventory::iter::<CordisInventory>.into_iter().count()
83}
84
85#[cfg(not(feature = "inventory"))]
86pub fn inventory_len() -> usize {
87 0
88}
89
90pub mod context;
91pub mod effect;
92pub mod error;
93pub mod events;
94pub mod fiber;
95pub mod logger;
96pub mod service;
97pub mod timer;
98
99pub use context::{Accessor, Context, EffectHandle};
100pub use effect::Disposable;
101pub use events::{summarize_listener_errors, AggregateError, Dispatch, EventsService};
102pub use error::{ValidationError, ValidationIssue};
103pub use fiber::{Fiber, FiberState, UndoMeta};
104pub use service::{CordisError, Service, ServiceInitFuture};
105
106pub mod events_catalog;
107pub use events_catalog::{contract_for, validate_dispatch, validate_listener, EventContract};
108pub mod events_payload;
109pub use events_payload::{
110 AgentAdmitEvent, AgentAdmitPayload, AgentCompletedEvent, AgentCompletedPayload,
111 AgentFailedEvent, AgentFailedPayload, AgentRunEvent, AgentRunRequest, AgentRunResult,
112 AgentStartedEvent, AgentStartedPayload, AgentUsageEvent, AgentUsagePayload, LlmCompleteEvent,
113 LlmCompleteRequest, LlmCompleteResult, LlmGenerateEvent, LlmGeneratePayload,
114 LlmGenerateToolsEvent, LlmGenerateToolsPayload, LlmGetClientEvent, LlmGetClientPayload,
115 LlmMessage, PipelineFanoutCompletedEvent, PipelineFanoutCompletedPayload,
116 PipelineStepFinishedEvent, PipelineStepFinishedPayload, PipelineStepStartedEvent,
117 PipelineStepStartedPayload, ScheduleDispatchedEvent, ScheduleDispatchedPayload,
118 SchedulerAdmitEvent, SchedulerAdmitPayload, SchedulerBeforeRunEvent, SchedulerBeforeRunPayload,
119 SchedulerTickEvent, SchedulerTickPayload, ServiceChangedEvent, ServiceChangedPayload,
120 ToolsExecuteEvent, ToolsExecutePayload, ToolsListEvent, ToolsListRequest, ToolsListResult,
121 ToolsResolveEvent, ToolsResolveRequest, TriggerFiredEvent, TriggerFiredPayload, TypedEvent,
122};
123pub mod loader;
124pub use loader::{
125 AppliedAction, CurrentEntries, Entry, EntryConfigFiller, EntryConfigFillerHandle, EntryTree,
126 EntryUpdate, Loader, LoaderOps,
127};
128
129pub mod cycles;
130pub use cycles::{find_dependency_cycle, DependencyGraph};
131
132pub mod reload;
133pub mod stamp;
134pub use reload::reload_entries_from_disk;
135pub use stamp::{FileStamp, ReloadOutcome};
136pub use watcher::SettleBarrier;
137
138pub mod metatheory;
139
140pub mod hmr;
141pub mod module_graph;
142pub mod registry;
143pub mod watcher;
144pub use registry::{Plugin, RegistryService};
145pub use module_graph::{ChangeOutcome, ModuleEntry, ModuleGraph, ModuleReload, NoopReload};
146
147pub use logger::{
148 derived_name, hyphenate, Exporter, ExporterConfig, LogArg, LogKind, LogLevel,
149 LoggerIntercept, LoggerService, Message,
150};
151
152pub mod compose;
153#[cfg(feature = "rhai")]
154pub mod rhai_service;
155pub mod worker;
156
157#[cfg(feature = "rhai")]
158pub use compose::{
159 compose_all, compose_entries, interpolate_config, resolve_includes, GROUP_PLUGIN,
160 INCLUDE_PLUGIN,
161};
162#[cfg(not(feature = "rhai"))]
163pub use compose::{compose_all, resolve_includes, GROUP_PLUGIN, INCLUDE_PLUGIN};
164#[cfg(feature = "rhai")]
165pub use rhai_service::{RhaiListenerConfig, RhaiPlugin, RhaiService, RhaiServiceConfig};
166
167pub type Symbol = String;
168pub type EventId = String;
169pub type FiberId = u64;
170
171pub fn compute_epoch(inject: &HashMap<TypeId, Symbol>) -> String {
172 if inject.is_empty() {
173 return ":".to_string();
174 }
175 let mut frags: Vec<String> = inject.values().cloned().collect();
176 frags.sort();
177 format!(":{}", frags.join(":"))
178}
179
180pub type PluginFactory =
194 Arc<dyn Fn(&Arc<Context>, &serde_json::Value) -> Result<FiberId, CordisError> + Send + Sync>;
195
196pub struct PluginRegistry {
203 factories: RwLock<HashMap<String, PluginFactory>>,
204}
205
206impl PluginRegistry {
207 pub fn new() -> Self {
208 Self {
209 factories: RwLock::new(HashMap::new()),
210 }
211 }
212
213 pub fn register(&self, name: &str, f: PluginFactory) {
214 self.factories.write().insert(name.to_string(), f);
215 }
216
217 pub fn get(&self, name: &str) -> Option<PluginFactory> {
218 self.factories.read().get(name).cloned()
219 }
220
221 pub fn names(&self) -> Vec<String> {
222 self.factories.read().keys().cloned().collect()
223 }
224}
225
226impl Default for PluginRegistry {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232impl Service for PluginRegistry {}
233
234fn block_on_plugin<S: Service + 'static>(
235 ctx: &Arc<Context>,
236 svc: S,
237) -> Result<FiberId, CordisError> {
238 tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(ctx.plugin(svc)))
239}
240
241pub fn factory_events_service(
243 ctx: &Arc<Context>,
244 _config: &serde_json::Value,
245) -> Result<FiberId, CordisError> {
246 block_on_plugin(ctx, EventsService::new())
247}
248
249#[cfg(feature = "rhai")]
251pub fn factory_rhai_policy(
252 ctx: &Arc<Context>,
253 config: &serde_json::Value,
254) -> Result<FiberId, CordisError> {
255 let cfg: RhaiServiceConfig = serde_json::from_value(config.clone())
256 .map_err(|e| CordisError::Configuration(format!("invalid RhaiPolicy config: {e}")))?;
257 tokio::task::block_in_place(|| {
258 tokio::runtime::Handle::current().block_on(ctx.plugin_with(RhaiPlugin, cfg))
259 })
260}
261
262pub fn register_plugins(reg: &PluginRegistry) {
264 reg.register("EventsService", Arc::new(factory_events_service));
265 #[cfg(feature = "rhai")]
266 reg.register("RhaiPolicy", Arc::new(factory_rhai_policy));
267}
268
269#[allow(dead_code)]
287pub struct ReflectService {
288 notifiers: RwLock<HashMap<TypeId, watch::Sender<()>>>,
289 dependents: RwLock<HashMap<TypeId, Vec<FiberId>>>,
290 fibers: RwLock<HashMap<FiberId, Arc<Fiber>>>,
291 fiber_provides: RwLock<HashMap<FiberId, TypeId>>,
292 ctx: RwLock<Option<Weak<Context>>>,
293}
294
295impl ReflectService {
296 pub fn new() -> Self {
297 Self {
298 notifiers: RwLock::new(HashMap::new()),
299 dependents: RwLock::new(HashMap::new()),
300 fibers: RwLock::new(HashMap::new()),
301 fiber_provides: RwLock::new(HashMap::new()),
302 ctx: RwLock::new(None),
303 }
304 }
305
306 pub fn ensure_notifier(&self, tid: TypeId) -> watch::Receiver<()> {
311 let mut notifiers = self.notifiers.write();
312 if let Some(sender) = notifiers.get(&tid) {
313 return sender.subscribe();
314 }
315 let (tx, rx) = watch::channel(());
316 notifiers.insert(tid, tx);
317 rx
318 }
319
320 pub fn ensure_notifier_for<T: Service>(&self) -> watch::Receiver<()> {
322 self.ensure_notifier(TypeId::of::<T>())
323 }
324
325 pub fn register_dependent(&self, tid: TypeId, fid: FiberId) {
328 let mut deps = self.dependents.write();
329 let entry = deps.entry(tid).or_default();
330 if !entry.contains(&fid) {
331 entry.push(fid);
332 }
333 }
334
335 pub fn register_fiber(&self, fid: FiberId, fiber: Arc<Fiber>, provides: TypeId) {
338 self.fibers.write().insert(fid, fiber);
339 self.fiber_provides.write().insert(fid, provides);
340 }
341
342 pub fn set_context(&self, ctx: &Arc<Context>) {
345 *self.ctx.write() = Some(Arc::downgrade(ctx));
346 }
347
348 pub fn notify(&self, tid: TypeId) {
354 self.prune_disposed();
355 let ctx_opt = self.ctx.read().as_ref().and_then(|w| w.upgrade());
357
358 if let Some(ctx) = &ctx_opt {
360 if let Some(events) = ctx.get::<EventsService>() {
361 let payload = crate::ServiceChangedPayload {
362 type_id: format!("{tid:?}"),
363 event: crate::events_catalog::ev::SERVICE_CHANGED.to_string(),
364 };
365 tokio::spawn(async move {
366 let _ = events
367 .dispatch_typed::<crate::ServiceChangedEvent>(&payload)
368 .await;
369 });
370 }
371 }
372
373 let mut queue = VecDeque::new();
374 let mut visited_type = HashSet::new();
375 let mut visited_fiber = HashSet::new();
376 queue.push_back(tid);
377 visited_type.insert(tid);
378 while let Some(cur) = queue.pop_front() {
379 if let Some(sender) = self.notifiers.read().get(&cur).cloned() {
381 let _ = sender.send(());
382 }
383 let fids = self
385 .dependents
386 .read()
387 .get(&cur)
388 .cloned()
389 .unwrap_or_default();
390 for fid in fids {
391 if !visited_fiber.insert(fid) {
392 continue;
393 }
394 let fiber_opt = self.fibers.read().get(&fid).cloned();
395 if let Some(fiber) = fiber_opt {
396 if let Some(ctx) = ctx_opt.clone() {
397 let fiber_clone = fiber.clone();
398 tokio::spawn(async move {
399 fiber_clone.refresh(&ctx).await;
400 });
401 }
402 if let Some(provided) = self.fiber_provides.read().get(&fid).copied() {
404 if visited_type.insert(provided) {
405 queue.push_back(provided);
406 }
407 }
408 }
409 }
410 }
411 }
412
413 #[allow(clippy::await_holding_lock)]
415 pub async fn notify_with_ctx(&self, tid: TypeId, ctx: &Arc<Context>) {
416 self.prune_disposed();
417 let mut queue = VecDeque::new();
418 let mut visited_type = HashSet::new();
419 let mut visited_fiber = HashSet::new();
420 queue.push_back(tid);
421 visited_type.insert(tid);
422 while let Some(cur) = queue.pop_front() {
423 if let Some(sender) = self.notifiers.read().get(&cur).cloned() {
424 let _ = sender.send(());
425 }
426 let fids = self
427 .dependents
428 .read()
429 .get(&cur)
430 .cloned()
431 .unwrap_or_default();
432 for fid in fids {
433 if !visited_fiber.insert(fid) {
434 continue;
435 }
436 let fiber = { self.fibers.read().get(&fid).cloned() };
437 if let Some(fiber) = fiber {
438 fiber.refresh(ctx).await;
439 if let Some(provided) = self.fiber_provides.read().get(&fid).copied() {
440 if visited_type.insert(provided) {
441 queue.push_back(provided);
442 }
443 }
444 }
445 }
446 }
447 }
448
449 pub fn prune_disposed(&self) -> usize {
458 let dead: Vec<FiberId> = self
459 .fibers
460 .read()
461 .iter()
462 .filter(|(_, fiber)| fiber.is_disposed())
463 .map(|(fid, _)| *fid)
464 .collect();
465 let mut removed = 0;
466 {
467 let mut fibers = self.fibers.write();
468 for fid in &dead {
469 if fibers.remove(fid).is_some() {
470 removed += 1;
471 }
472 }
473 }
474 self.fiber_provides
475 .write()
476 .retain(|fid, _| !dead.contains(fid));
477 removed
478 }
479
480 pub fn subscribe(&self, tid: TypeId) -> Option<watch::Receiver<()>> {
482 self.notifiers.read().get(&tid).map(|s| s.subscribe())
483 }
484}
485
486impl Default for ReflectService {
487 fn default() -> Self {
488 Self::new()
489 }
490}
491
492impl Service for ReflectService {}
493
494#[derive(Debug, Clone, PartialEq, Eq, Default)]
512pub struct JournalRecord {
513 pub plugin: String,
514 pub config: serde_json::Value,
515 pub fiber_id: Option<FiberId>,
516 pub generation: u64,
517}
518
519#[derive(Clone, Default)]
530pub struct LoaderJournal {
531 records: Arc<RwLock<HashMap<String, JournalRecord>>>,
532}
533
534impl LoaderJournal {
535 pub fn new() -> Self {
536 Self::default()
537 }
538
539 pub fn provide_new(ctx: &std::sync::Arc<Context>) -> std::sync::Arc<Self> {
541 let journal = std::sync::Arc::new(Self::default());
542 ctx.provide_arc(journal.clone());
543 journal
544 }
545
546 pub fn upsert(
549 &self,
550 id: &str,
551 plugin: &str,
552 config: serde_json::Value,
553 fiber_id: Option<FiberId>,
554 ) {
555 let mut records = self.records.write();
556 let generation = records.get(id).map(|r| r.generation).unwrap_or(0) + 1;
557 records.insert(
558 id.to_string(),
559 JournalRecord {
560 plugin: plugin.to_string(),
561 config,
562 fiber_id,
563 generation,
564 },
565 );
566 }
567
568 pub fn update_config(
571 &self,
572 id: &str,
573 new_config: serde_json::Value,
574 fiber_id: Option<FiberId>,
575 ) -> Option<JournalRecord> {
576 let mut records = self.records.write();
577 let record = records.get_mut(id)?;
578 record.config = new_config;
579 if let Some(fid) = fiber_id {
580 record.fiber_id = Some(fid);
581 }
582 record.generation += 1;
583 Some(record.clone())
584 }
585
586 pub fn retire(&self, id: &str) -> Option<JournalRecord> {
588 self.records.write().remove(id)
589 }
590 pub fn rename(&self, old: &str, new: &str) -> Option<JournalRecord> {
596 let mut records = self.records.write();
597 let record = records.remove(old)?;
598 records.insert(new.to_string(), record.clone());
599 Some(record)
600 }
601
602 pub fn get(&self, id: &str) -> Option<JournalRecord> {
603 self.records.read().get(id).cloned()
604 }
605
606 pub fn len(&self) -> usize {
607 self.records.read().len()
608 }
609
610 pub fn is_empty(&self) -> bool {
611 self.records.read().is_empty()
612 }
613}
614
615impl Service for LoaderJournal {}
616
617#[cfg(test)]
622mod tests {
623 use super::*;
624 use parking_lot::Mutex;
625
626 #[test]
627 fn inventory_len_is_kernel_only() {
628 #[cfg(feature = "inventory")]
629 assert_eq!(inventory_len(), 4);
630 #[cfg(not(feature = "inventory"))]
631 assert_eq!(inventory_len(), 0);
632 }
633
634 #[derive(Debug)]
635 struct FooService(pub i32);
636 impl Service for FooService {}
637
638 #[derive(Debug)]
639 struct BarService(pub i32);
640 impl Service for BarService {}
641
642 #[derive(Debug)]
643 struct ConsumerService;
644 impl Service for ConsumerService {}
645
646 #[tokio::test]
647 async fn temporal_composability() {
648 let ctx = Context::new_root();
650 let pre_len = ctx.snapshot_len();
651 assert!(ctx.get::<BarService>().is_none());
652
653 let bar = ctx.provide(BarService(42));
655 assert_eq!(bar.0, 42);
656 assert!(ctx.get::<BarService>().is_some());
657 assert_eq!(ctx.snapshot_len(), pre_len + 1);
658
659 let _ = ctx.fiber().dispose().await;
661 assert!(ctx.get::<BarService>().is_none());
662 assert_eq!(ctx.snapshot_len(), pre_len);
663 }
664
665 #[tokio::test]
666 async fn spatial_composability() {
667 let ctx = Context::new_root();
669 let consumer_fiber = Arc::new(Fiber::new());
670 consumer_fiber.declare_inject::<FooService>();
671
672 assert_eq!(consumer_fiber.state(), FiberState::Inactive { error: None });
674 assert_eq!(consumer_fiber.epoch(), "");
675
676 ctx.provide(FooService(1));
678 consumer_fiber.refresh(&ctx).await;
679 assert!(matches!(consumer_fiber.state(), FiberState::Active { .. }));
680 let epoch_v1 = consumer_fiber.epoch();
681 assert!(epoch_v1.contains("FooService"));
682 assert!(epoch_v1.contains(":1") || epoch_v1.contains("1"));
683
684 ctx.provide(FooService(2));
686 let prev_epoch = epoch_v1.clone();
687 consumer_fiber.refresh(&ctx).await;
688 let epoch_v2 = consumer_fiber.epoch();
689 assert_ne!(prev_epoch, epoch_v2);
690 assert!(matches!(consumer_fiber.state(), FiberState::Active { .. }));
691 assert_eq!(ctx.get::<FooService>().unwrap().0, 2);
693 }
694
695 #[tokio::test]
696 async fn isolate_and_intercept() {
697 let root = Context::new_root();
698 root.provide(FooService(10));
699 assert_eq!(root.get::<FooService>().unwrap().0, 10);
700
701 let tenant_ctx = root.isolate::<FooService>("tenant:acme");
703 tenant_ctx.provide(FooService(99));
707 assert_eq!(tenant_ctx.get::<FooService>().unwrap().0, 99);
708 assert_eq!(root.get::<FooService>().unwrap().0, 10);
709
710 let req_ctx = root.intercept(FooService(77));
712 assert_eq!(req_ctx.get::<FooService>().unwrap().0, 77);
713 assert_eq!(root.get::<FooService>().unwrap().0, 10);
715 }
716
717 #[tokio::test]
718 async fn events_dispatch_modes() {
719 let svc = EventsService::new();
720 svc.on("test".into(), |v| async move {
721 let n = v.as_i64().unwrap_or(0);
722 Ok(serde_json::Value::Number((n + 1).into()))
723 });
724 let out = svc
725 .dispatch(
726 "test".into(),
727 serde_json::Value::Number(1.into()),
728 Dispatch::Serial,
729 )
730 .await
731 .unwrap();
732 assert_eq!(out, serde_json::Value::Number(2.into()));
733 }
734
735 #[tokio::test]
736 async fn epoch_monoid() {
737 let mut map = HashMap::new();
738 map.insert(TypeId::of::<FooService>(), "uid1".to_string());
739 map.insert(TypeId::of::<BarService>(), "uid2".to_string());
740 let e = compute_epoch(&map);
741 assert!(e.starts_with(':'));
742 assert!(e.contains("uid1"));
743 assert!(e.contains("uid2"));
744 let empty: HashMap<TypeId, Symbol> = HashMap::new();
746 assert_eq!(compute_epoch(&empty), ":");
747 }
748
749 #[tokio::test]
750 async fn fiber_inertia_serializes_transitions() {
751 let fiber = Arc::new(Fiber::new());
752 fiber.declare_inject::<FooService>();
753 let ctx = Context::new_root();
754 let f1 = fiber.clone();
756 let c1 = ctx.clone();
757 let f2 = fiber.clone();
758 let c2 = ctx.clone();
759 let (r1, r2) = tokio::join!(f1.refresh(&c1), f2.refresh(&c2));
760 let _ = (r1, r2);
762 assert!(matches!(
763 fiber.state(),
764 FiberState::Inactive { .. } | FiberState::Active { .. }
765 ));
766 }
767
768 #[tokio::test]
769 async fn registry_single_source_discipline() {
770 let ctx = Context::new_root();
771 let registry = RegistryService::new();
772
773 struct FooPlugin;
774 impl Plugin for FooPlugin {
775 type Config = ();
776 type Provides = FooService;
777 fn apply(
778 &self,
779 _ctx: &Arc<Context>,
780 _cfg: Self::Config,
781 ) -> Result<Arc<Self::Provides>, CordisError> {
782 Ok(Arc::new(FooService(1)))
783 }
784 }
785
786 struct FooPlugin2;
787 impl Plugin for FooPlugin2 {
788 type Config = ();
789 type Provides = FooService;
790 fn apply(
791 &self,
792 _ctx: &Arc<Context>,
793 _cfg: Self::Config,
794 ) -> Result<Arc<Self::Provides>, CordisError> {
795 Ok(Arc::new(FooService(2)))
796 }
797 }
798
799 let fid1 = registry
800 .plugin(&ctx, FooPlugin, ())
801 .expect("first plugin ok");
802 assert!(registry.get_fiber(fid1).is_some());
803 let err = registry
804 .plugin(&ctx, FooPlugin2, ())
805 .expect_err("duplicate should fail");
806 assert!(err.to_string().contains("duplicate provider"));
807 assert!(registry.get_fiber(fid1).is_some());
809 }
810
811 #[tokio::test]
812 async fn test_event_bus_dispatch_received() {
813 let ctx = Context::new_root();
814 let events = ctx.provide(EventsService::new());
815
816 let received = Arc::new(Mutex::new(Vec::new()));
818 let received_clone = received.clone();
819 events.on("test.event".into(), move |payload| {
820 let r = received_clone.clone();
821 async move {
822 r.lock().push(payload.clone());
823 Ok(payload)
824 }
825 });
826
827 let payload = serde_json::json!({"key": "value"});
829 events
830 .dispatch("test.event".into(), payload.clone(), Dispatch::Serial)
831 .await
832 .unwrap();
833
834 let msgs = received.lock();
836 assert_eq!(msgs.len(), 1);
837 assert_eq!(msgs[0]["key"], "value");
838 }
839
840 #[tokio::test]
841 async fn test_reactive_activation_deactivation() {
842 struct DepService;
844 impl Service for DepService {}
845
846 let ctx = Context::new_root();
848 ctx.provide(ReflectService::new());
849 let reflect = ctx.get::<ReflectService>().unwrap();
850 reflect.set_context(&ctx);
851
852 let fiber = Arc::new(Fiber::new());
854 fiber.declare_inject::<DepService>();
855 let fid: FiberId = 100;
856 reflect.register_dependent(TypeId::of::<DepService>(), fid);
857 reflect.register_fiber(fid, fiber.clone(), TypeId::of::<DepService>());
858
859 fiber.refresh(&ctx).await;
861 assert!(matches!(fiber.state(), FiberState::Inactive { .. }));
862
863 ctx.provide(DepService);
865 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
867 assert!(
868 matches!(fiber.state(), FiberState::Active { .. }),
869 "fiber should be Active after provide, got: {:?}",
870 fiber.state()
871 );
872
873 let _ = ctx.remove::<DepService>();
875 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
876 assert!(
877 matches!(fiber.state(), FiberState::Inactive { .. }),
878 "fiber should be Inactive after remove, got: {:?}",
879 fiber.state()
880 );
881 }
882
883 #[tokio::test]
884 async fn test_isolate_disjoint_namespaces() {
885 #[derive(Debug)]
886 struct ToolSvc(String);
887 impl Service for ToolSvc {}
888
889 let root = Context::new_root();
890
891 let ctx_a = root.isolate::<ToolSvc>("tenant_a");
893 ctx_a.provide(ToolSvc("tool_for_a".into()));
894
895 let ctx_b = root.isolate::<ToolSvc>("tenant_b");
896 ctx_b.provide(ToolSvc("tool_for_b".into()));
897
898 let svc_a = ctx_a.get_isolated::<ToolSvc>("tenant_a");
900 assert!(svc_a.is_some());
901 assert_eq!(svc_a.unwrap().0, "tool_for_a");
902
903 let svc_b = ctx_b.get_isolated::<ToolSvc>("tenant_b");
904 assert!(svc_b.is_some());
905 assert_eq!(svc_b.unwrap().0, "tool_for_b");
906
907 assert!(ctx_a.get_isolated::<ToolSvc>("tenant_b").is_none());
909 assert!(ctx_b.get_isolated::<ToolSvc>("tenant_a").is_none());
910
911 assert!(root.get_isolated::<ToolSvc>("tenant_a").is_none());
913 assert!(root.get_isolated::<ToolSvc>("tenant_b").is_none());
914 }
915
916 #[test]
917 fn bind_isolate_labels_provided_service_in_place() {
918 #[derive(Debug)]
919 struct ToolSvc(String);
920 impl Service for ToolSvc {}
921
922 let root = Context::new_root();
923 root.provide(ToolSvc("fleet".into()));
924 root.bind_isolate(TypeId::of::<ToolSvc>(), "tenant:acme");
925 let got = root
926 .get_isolated::<ToolSvc>("tenant:acme")
927 .expect("in-place isolate");
928 assert_eq!(got.0, "fleet");
929 assert!(root.get::<ToolSvc>().is_some());
930 }
931
932 #[tokio::test]
933 async fn test_intercept_overrides_get() {
934 #[derive(Debug)]
935 struct ModelSvc {
936 model: String,
937 }
938 impl Service for ModelSvc {}
939
940 let root = Context::new_root();
941 root.provide(ModelSvc {
942 model: "gpt-4".into(),
943 });
944
945 assert_eq!(root.get::<ModelSvc>().unwrap().model, "gpt-4");
947
948 let req_ctx = root.with_intercept(ModelSvc {
950 model: "gpt-4o-mini".into(),
951 });
952 assert_eq!(req_ctx.get::<ModelSvc>().unwrap().model, "gpt-4o-mini");
953
954 assert_eq!(root.get::<ModelSvc>().unwrap().model, "gpt-4");
956
957 let inner_ctx = req_ctx.intercept(ModelSvc {
959 model: "o1-preview".into(),
960 });
961 assert_eq!(inner_ctx.get::<ModelSvc>().unwrap().model, "o1-preview");
962 assert_eq!(req_ctx.get::<ModelSvc>().unwrap().model, "gpt-4o-mini");
964 }
965
966 #[tokio::test]
967 async fn isolate_wins_over_same_type_intercept() {
968 #[derive(Debug)]
969 struct ToolSvc(String);
970 impl Service for ToolSvc {}
971
972 #[derive(Debug)]
973 struct OtherSvc(String);
974 impl Service for OtherSvc {}
975
976 let root = Context::new_root();
977 let child = root.isolate::<ToolSvc>("acme");
978 child.provide(ToolSvc("store".into()));
979
980 let intercepted = child.intercept(ToolSvc("override".into()));
981 assert_eq!(intercepted.get::<ToolSvc>().unwrap().0, "store");
982
983 let mixed = child.intercept(OtherSvc("override".into()));
984 assert_eq!(mixed.get::<OtherSvc>().unwrap().0, "override");
985 assert_eq!(mixed.get::<ToolSvc>().unwrap().0, "store");
986 }
987
988 #[tokio::test]
989 async fn inject_returns_immediately_when_already_provided() {
990 let ctx = Context::new_root();
991 ctx.provide(FooService(1));
992 let got = ctx.inject::<FooService>().await;
993 assert_eq!(got.name(), FooService(1).name());
994 assert_eq!(got.0, 1);
995 }
996
997 #[tokio::test]
998 async fn inject_waits_until_service_is_provided() {
999 let ctx = Context::new_root();
1000 let waiter = ctx.clone();
1001 let handle = tokio::spawn(async move { waiter.inject::<FooService>().await });
1002 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
1003 ctx.provide(FooService(42));
1004 let got = tokio::time::timeout(std::time::Duration::from_millis(200), handle)
1005 .await
1006 .expect("inject should complete within 200ms")
1007 .expect("inject task should not panic");
1008 assert_eq!(got.0, 42);
1009 }
1010
1011 #[tokio::test]
1012 async fn inject_unblocks_via_reflect_notify() {
1013 let ctx = Context::new_root();
1014 ctx.provide(ReflectService::new());
1015 let reflect = ctx.get::<ReflectService>().unwrap();
1016 reflect.set_context(&ctx);
1017
1018 let waiter = ctx.clone();
1019 let handle = tokio::spawn(async move { waiter.inject::<FooService>().await });
1020 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1021 ctx.provide(FooService(7));
1022 let got = tokio::time::timeout(std::time::Duration::from_millis(200), handle)
1023 .await
1024 .expect("inject should complete within 200ms via reflect notify")
1025 .expect("inject task should not panic");
1026 assert_eq!(got.0, 7);
1027 }
1028
1029 #[tokio::test]
1030 async fn test_production_style_reactive_cycle() {
1031 #[derive(Debug)]
1032 struct Probe;
1033 impl Service for Probe {}
1034
1035 let ctx = Context::new_root();
1036 ctx.provide(ReflectService::new());
1037 let reflect = ctx.get::<ReflectService>().unwrap();
1038 reflect.set_context(&ctx);
1039
1040 let f = Arc::new(Fiber::new());
1041 f.declare_inject::<Probe>();
1042 reflect.register_dependent(TypeId::of::<Probe>(), 777);
1043 reflect.register_fiber(777, f.clone(), TypeId::of::<Probe>());
1044
1045 f.refresh(&ctx).await;
1047 assert!(
1048 matches!(f.state(), FiberState::Inactive { .. }),
1049 "expected Inactive before provide, got {:?}",
1050 f.state()
1051 );
1052
1053 let _probe = ctx.provide(Probe);
1055 reflect.notify_with_ctx(TypeId::of::<Probe>(), &ctx).await;
1056 assert!(
1057 matches!(f.state(), FiberState::Active { .. }),
1058 "expected Active after provide, got {:?}",
1059 f.state()
1060 );
1061
1062 let _ = ctx.remove::<Probe>();
1064 reflect.notify_with_ctx(TypeId::of::<Probe>(), &ctx).await;
1065 assert!(
1066 matches!(f.state(), FiberState::Inactive { .. }),
1067 "expected Inactive after remove, got {:?}",
1068 f.state()
1069 );
1070 }
1071
1072 use std::sync::atomic::{AtomicUsize, Ordering};
1077
1078 #[tokio::test]
1079 async fn events_emit_fire_and_forget_and_broadcast() {
1080 let svc = EventsService::new();
1081 let (done_tx, mut done_rx) = tokio::sync::mpsc::channel::<()>(16);
1083 let mut bus_rx = svc.subscribe();
1084
1085 for i in 0..3 {
1086 let tx = done_tx.clone();
1087 svc.on("emit.test".into(), move |payload| {
1088 let tx = tx.clone();
1089 async move {
1090 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1092 let _ = tx.send(()).await;
1093 Ok(serde_json::json!({ "handler": i, "seen": payload }))
1094 }
1095 });
1096 }
1097
1098 let payload = serde_json::json!({ "n": 1 });
1099 let start = std::time::Instant::now();
1100 let out = svc
1101 .dispatch("emit.test".into(), payload.clone(), Dispatch::Emit)
1102 .await
1103 .unwrap();
1104 let dispatch_elapsed = start.elapsed();
1105
1106 assert_eq!(out, serde_json::Value::Null);
1109 assert!(
1110 dispatch_elapsed < std::time::Duration::from_millis(20),
1111 "emit returned after {:?} — should return immediately",
1112 dispatch_elapsed
1113 );
1114
1115 let (evt, bus_payload) =
1117 tokio::time::timeout(std::time::Duration::from_secs(1), bus_rx.recv())
1118 .await
1119 .expect("bus should broadcast")
1120 .expect("bus recv should be a value");
1121 assert_eq!(evt, "emit.test");
1122 assert_eq!(bus_payload, payload);
1123
1124 for _ in 0..3 {
1127 tokio::time::timeout(std::time::Duration::from_secs(1), done_rx.recv())
1128 .await
1129 .expect("handlers should complete")
1130 .expect("handler completion signal");
1131 }
1132 }
1133
1134 #[tokio::test]
1135 async fn events_emit_invokes_registered_handler_counter() {
1136 let svc = EventsService::new();
1140 let counter = Arc::new(AtomicUsize::new(0));
1141
1142 let c = counter.clone();
1143 svc.on("emit.counter".into(), move |payload| {
1144 let c = c.clone();
1145 async move {
1146 let n = payload.as_i64().unwrap_or(0);
1148 for _ in 0..n {
1149 tokio::task::yield_now().await;
1150 }
1151 c.fetch_add(1, Ordering::SeqCst);
1152 Ok(serde_json::Value::Null)
1153 }
1154 });
1155
1156 let out = svc
1157 .dispatch("emit.counter".into(), serde_json::json!(5), Dispatch::Emit)
1158 .await
1159 .unwrap();
1160 assert_eq!(out, serde_json::Value::Null);
1161
1162 for _ in 0..100 {
1165 if counter.load(Ordering::SeqCst) > 0 {
1166 break;
1167 }
1168 tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1169 }
1170 assert!(
1171 counter.load(Ordering::SeqCst) > 0,
1172 "emit handler should have run and incremented the counter"
1173 );
1174 }
1175
1176 #[tokio::test]
1177 async fn events_serial_threads_payload_in_order() {
1178 let svc = EventsService::new();
1179 let payload = serde_json::json!({ "n": 1 });
1180 let seen = Arc::new(Mutex::new(Vec::new()));
1181
1182 for tag in ["a", "b", "c"] {
1183 let seen = seen.clone();
1184 let tag = tag.to_string();
1185 svc.on("serial.test".into(), move |received| {
1186 let seen = seen.clone();
1187 let tag = tag.clone();
1188 async move {
1189 seen.lock().push((tag, received));
1190 Ok(serde_json::Value::Null)
1191 }
1192 });
1193 }
1194
1195 let out = svc
1196 .dispatch("serial.test".into(), payload.clone(), Dispatch::Serial)
1197 .await
1198 .unwrap();
1199
1200 assert_eq!(out, payload);
1202 assert_eq!(
1203 seen.lock().clone(),
1204 vec![
1205 ("a".to_string(), payload.clone()),
1206 ("b".to_string(), payload.clone()),
1207 ("c".to_string(), payload),
1208 ]
1209 );
1210 }
1211
1212 #[tokio::test]
1213 async fn events_bail_stops_at_first_non_null_and_skips_later_handlers() {
1214 let svc = EventsService::new();
1215 let ran = Arc::new(AtomicUsize::new(0));
1216
1217 let h1 = ran.clone();
1219 svc.on("bail.test".into(), move |_payload| {
1220 let r = h1.clone();
1221 async move {
1222 r.fetch_add(1, Ordering::SeqCst);
1223 Ok(serde_json::Value::Null)
1224 }
1225 });
1226 let h2 = ran.clone();
1228 svc.on("bail.test".into(), move |_payload| {
1229 let r = h2.clone();
1230 async move {
1231 r.fetch_add(1, Ordering::SeqCst);
1232 Ok(serde_json::json!({ "bailed": true }))
1233 }
1234 });
1235 let h3 = ran.clone();
1237 svc.on("bail.test".into(), move |_payload| {
1238 let r = h3.clone();
1239 async move {
1240 r.fetch_add(1, Ordering::SeqCst);
1241 Ok(serde_json::Value::Null)
1242 }
1243 });
1244
1245 let payload = serde_json::json!({ "n": 1 });
1246 let out = svc
1247 .dispatch("bail.test".into(), payload.clone(), Dispatch::Bail)
1248 .await
1249 .unwrap();
1250 assert_eq!(out, serde_json::json!({ "bailed": true }));
1251 assert_eq!(ran.load(Ordering::SeqCst), 2);
1253 }
1254
1255 #[tokio::test]
1256 async fn events_waterfall_handler_calls_next_and_receives_downstream_result() {
1257 let svc = EventsService::new();
1258 svc.on_waterfall("wf.next".into(), |payload, next| {
1261 let next = next;
1262 async move {
1263 let downstream = next(payload).await?;
1264 let mut obj = downstream.as_object().cloned().unwrap_or_default();
1266 obj.insert("outer".into(), serde_json::json!(true));
1267 Ok(serde_json::Value::Object(obj))
1268 }
1269 });
1270 svc.on_waterfall("wf.next".into(), |payload, _next| async move {
1271 let mut obj = payload.as_object().cloned().unwrap_or_default();
1272 obj.insert("inner_seen".into(), serde_json::json!(payload.get("value")));
1273 Ok(serde_json::Value::Object(obj))
1274 });
1275
1276 let payload = serde_json::json!({ "value": 42 });
1277 let out = svc
1278 .dispatch("wf.next".into(), payload, Dispatch::Waterfall)
1279 .await
1280 .unwrap();
1281 let obj = out
1282 .as_object()
1283 .expect("waterfall output should be an object");
1284 assert_eq!(obj["inner_seen"], serde_json::json!(42));
1286 assert_eq!(obj["outer"], serde_json::json!(true));
1287 }
1288
1289 #[tokio::test]
1290 async fn events_waterfall_handler_short_circuits_skips_later_handlers() {
1291 let svc = EventsService::new();
1292 let ran = Arc::new(AtomicUsize::new(0));
1293
1294 let h1 = ran.clone();
1296 svc.on_waterfall("wf.short".into(), move |_payload, _next| {
1297 let r = h1.clone();
1298 async move {
1299 r.fetch_add(1, Ordering::SeqCst);
1300 Ok(serde_json::json!({ "owned": true }))
1301 }
1302 });
1303 let h2 = ran.clone();
1305 svc.on_waterfall("wf.short".into(), move |payload, next| {
1306 let r = h2.clone();
1307 async move {
1308 r.fetch_add(1, Ordering::SeqCst);
1309 next(payload).await
1310 }
1311 });
1312
1313 let payload = serde_json::json!({ "n": 1 });
1314 let out = svc
1315 .dispatch("wf.short".into(), payload, Dispatch::Waterfall)
1316 .await
1317 .unwrap();
1318 assert_eq!(out, serde_json::json!({ "owned": true }));
1319 assert_eq!(ran.load(Ordering::SeqCst), 1);
1321 }
1322
1323 #[tokio::test]
1324 async fn events_waterfall_empty_chain_returns_payload_unchanged() {
1325 let svc = EventsService::new();
1326 let payload = serde_json::json!({ "n": 7 });
1327 let out = svc
1328 .dispatch("wf.empty".into(), payload.clone(), Dispatch::Waterfall)
1329 .await
1330 .unwrap();
1331 assert_eq!(out, payload);
1332 }
1333
1334 #[tokio::test]
1335 async fn events_parallel_propagates_aggregate_error() {
1336 let svc = EventsService::new();
1337 svc.on("par.test".into(), |_payload| async move {
1338 Ok(serde_json::json!({ "ok": 1 }))
1339 });
1340 svc.on("par.test".into(), |_payload| async move {
1341 Err(CordisError::Fiber("boom".into()))
1342 });
1343 svc.on("par.test".into(), |_payload| async move {
1344 Ok(serde_json::json!({ "ok": 2 }))
1345 });
1346
1347 let payload = serde_json::json!({ "n": 1 });
1348 let err = svc
1349 .dispatch("par.test".into(), payload, Dispatch::Parallel)
1350 .await
1351 .unwrap_err();
1352 assert!(
1353 err.to_string().contains("boom"),
1354 "parallel should propagate the handler error, got: {err}"
1355 );
1356 }
1357
1358 #[tokio::test]
1359 async fn events_parallel_returns_a_value_when_no_handler_errors() {
1360 let svc = EventsService::new();
1361 let payload = serde_json::json!({ "n": 1 });
1362 let seen = Arc::new(Mutex::new(Vec::new()));
1363
1364 for tag in ["a", "b"] {
1365 let seen = seen.clone();
1366 let tag = tag.to_string();
1367 svc.on("par2.test".into(), move |received| {
1368 let seen = seen.clone();
1369 let tag = tag.clone();
1370 async move {
1371 seen.lock().push((tag, received));
1372 Ok(serde_json::json!({ "handler": "complete" }))
1373 }
1374 });
1375 }
1376
1377 let out = svc
1378 .dispatch("par2.test".into(), payload.clone(), Dispatch::Parallel)
1379 .await
1380 .unwrap();
1381
1382 assert_eq!(out, serde_json::Value::Null);
1384 let mut completed = seen.lock().clone();
1385 completed.sort_by(|left, right| left.0.cmp(&right.0));
1386 assert_eq!(
1387 completed,
1388 vec![
1389 ("a".to_string(), payload.clone()),
1390 ("b".to_string(), payload)
1391 ]
1392 );
1393 }
1394
1395 #[tokio::test(flavor = "multi_thread")]
1396 async fn notify_broadcasts_service_changed_event() {
1397 let ctx = Context::new_root();
1398 let events_handle = ctx.provide(EventsService::new());
1399 let reflect = ctx.provide(ReflectService::new());
1400 reflect.set_context(&ctx);
1401
1402 let mut rx = events_handle.subscribe();
1403 reflect.notify(TypeId::of::<u64>());
1404
1405 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(500);
1406 let mut seen = false;
1407 while std::time::Instant::now() < deadline {
1408 match rx.try_recv() {
1409 Ok((name, payload)) => {
1410 assert_eq!(name, crate::events_catalog::ev::SERVICE_CHANGED);
1411 assert!(
1413 payload["type_id"].as_str().unwrap().starts_with("TypeId("),
1414 "payload should identify the changed type: {payload}"
1415 );
1416 seen = true;
1417 break;
1418 }
1419 Err(tokio::sync::broadcast::error::TryRecvError::Empty) => {
1420 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
1421 }
1422 Err(e) => panic!("unexpected broadcast error: {e}"),
1423 }
1424 }
1425 assert!(
1426 seen,
1427 "service.changed broadcast not observed within timeout"
1428 );
1429 }
1430
1431 #[tokio::test]
1436 async fn reflect_prune_disposed_drops_dead_fibers_only() {
1437 let ctx = Context::new_root();
1438 let reflect = ReflectService::new();
1439
1440 let dead = Arc::new(Fiber::new());
1441 let live = Arc::new(Fiber::new());
1442 let failed = Arc::new(Fiber::new());
1443 failed.set_state(crate::FiberState::Failed { error: None });
1444
1445 reflect.register_fiber(1, dead.clone(), TypeId::of::<u64>());
1446 reflect.register_fiber(2, live.clone(), TypeId::of::<u64>());
1447 reflect.register_fiber(3, failed.clone(), TypeId::of::<u64>());
1448
1449 assert_eq!(reflect.prune_disposed(), 0);
1451
1452 let _ = dead.dispose().await;
1453 assert_eq!(
1454 reflect.prune_disposed(),
1455 1,
1456 "exactly the disposed fiber is dropped"
1457 );
1458 assert!(matches!(live.state(), crate::FiberState::Inactive { .. }));
1460 assert!(matches!(failed.state(), crate::FiberState::Failed { .. }));
1461
1462 reflect.notify(TypeId::of::<u64>());
1464 }
1465}