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