Skip to main content

cordis/
context.rs

1//! Root and child contexts tying all Cordis services together.
2
3use crate::effect::{AsyncDisposer, EffectHandle};
4use crate::events::{Event, EventOptions, EventResult, EventValue, EventsRoot, EventsService};
5use crate::fiber::{Fiber, FiberInner};
6use crate::logger::{LogArg, Logger, LoggerRoot, LoggerService};
7use crate::reflect::{Accessor, ReflectRoot, ReflectService};
8use crate::registry::{Inject, IntoPlugin, PluginOutput, RegistryRoot, RegistryService};
9use crate::{Config, Result, Value};
10use std::collections::HashMap;
11use std::fmt::{self, Debug, Formatter};
12use std::future::Future;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, OnceLock, Weak};
15
16/// Opaque service-scope label used by [`Context::isolate_with`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct Isolation(pub(crate) u64);
19
20impl Isolation {
21    /// Create a user-defined label.
22    ///
23    /// Labels returned by [`Context::new_isolation`] are preferred because
24    /// they cannot accidentally collide with framework-generated labels.
25    pub const fn from_raw(value: u64) -> Self {
26        Self(value)
27    }
28
29    /// Expose the numeric label for persistence or diagnostics.
30    pub const fn as_raw(self) -> u64 {
31        self.0
32    }
33}
34
35/// Event listener filter attached to a child context.
36pub type ContextFilter = Arc<dyn Fn(&Context) -> bool + Send + Sync + 'static>;
37
38/// Immutable data inherited by contexts and copied on extension.
39#[derive(Clone, Default)]
40pub struct ContextMeta {
41    pub(crate) isolates: Arc<HashMap<String, Isolation>>,
42    pub(crate) intercepts: Arc<Vec<(String, Value)>>,
43    pub(crate) values: Arc<HashMap<String, Value>>,
44    pub(crate) filter: Option<ContextFilter>,
45    pub(crate) base_url: Option<Arc<str>>,
46}
47
48impl Debug for ContextMeta {
49    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
50        f.debug_struct("ContextMeta")
51            .field("isolates", &self.isolates)
52            .field("intercept_count", &self.intercepts.len())
53            .field("value_keys", &self.values.keys().collect::<Vec<_>>())
54            .field("has_filter", &self.filter.is_some())
55            .field("base_url", &self.base_url)
56            .finish()
57    }
58}
59
60pub(crate) struct RootInner {
61    pub(crate) reflect: ReflectRoot,
62    pub(crate) registry: RegistryRoot,
63    pub(crate) events: EventsRoot,
64    pub(crate) logger: LoggerRoot,
65    pub(crate) next_scope: AtomicU64,
66    pub(crate) next_fiber: AtomicU64,
67    pub(crate) next_effect: AtomicU64,
68    pub(crate) root_fiber: OnceLock<Fiber>,
69}
70
71impl RootInner {
72    fn new() -> Self {
73        Self {
74            reflect: ReflectRoot::new(),
75            registry: RegistryRoot::new(),
76            events: EventsRoot::new(),
77            logger: LoggerRoot::new(),
78            // Keep 0 reserved for an absent scope/fiber/effect.
79            next_scope: AtomicU64::new(0),
80            next_fiber: AtomicU64::new(0),
81            next_effect: AtomicU64::new(0),
82            root_fiber: OnceLock::new(),
83        }
84    }
85
86    pub(crate) fn scope(&self) -> Isolation {
87        Isolation(self.next_scope.fetch_add(1, Ordering::Relaxed) + 1)
88    }
89
90    pub(crate) fn fiber_id(&self) -> u64 {
91        self.next_fiber.fetch_add(1, Ordering::Relaxed) + 1
92    }
93
94    pub(crate) fn effect_id(&self) -> u64 {
95        self.next_effect.fetch_add(1, Ordering::Relaxed) + 1
96    }
97}
98
99/// Root and child dependency containers for Cordis plugins.
100///
101/// Cloning a context is cheap. Child contexts share all runtime services while
102/// carrying immutable scope/intercept/metadata overlays and the current fiber.
103#[derive(Clone)]
104pub struct Context {
105    pub(crate) root: Arc<RootInner>,
106    pub(crate) fiber: Weak<FiberInner>,
107    pub(crate) meta: ContextMeta,
108}
109
110impl Context {
111    /// Create a root context and install the core reflection, registry, event,
112    /// and logger services.
113    pub fn new() -> Self {
114        let root = Arc::new(RootInner::new());
115        let fiber = Fiber::new_root(Arc::downgrade(&root), ContextMeta::default());
116        root.root_fiber
117            .set(fiber.clone())
118            .unwrap_or_else(|_| unreachable!("new root has no fiber"));
119        fiber.context().expect("fresh root is alive")
120    }
121
122    /// Return whether two contexts belong to the same root application.
123    pub fn same_root(&self, other: &Context) -> bool {
124        Arc::ptr_eq(&self.root, &other.root)
125    }
126
127    /// Return the root context.
128    pub fn root(&self) -> Context {
129        self.root
130            .root_fiber
131            .get()
132            .expect("root fiber initialized")
133            .context()
134            .expect("root context has a live root")
135    }
136
137    /// Return this context's owning plugin fiber.
138    pub fn fiber(&self) -> Result<Fiber> {
139        self.fiber
140            .upgrade()
141            .map(Fiber::from_inner)
142            .ok_or_else(|| crate::CordisError::new(crate::ErrorCode::InactiveEffect))
143    }
144
145    /// Return the base URL inherited by this context, when set.
146    pub fn base_url(&self) -> Option<&str> {
147        self.meta.base_url.as_deref()
148    }
149
150    /// Derive a child context with a new base URL.
151    pub fn with_base_url(&self, base_url: impl Into<Arc<str>>) -> Context {
152        let mut child = self.clone();
153        child.meta.base_url = Some(base_url.into());
154        child
155    }
156
157    /// Derive a child context carrying arbitrary metadata.
158    pub fn extend<T>(&self, name: impl Into<String>, value: T) -> Context
159    where
160        T: Send + Sync + 'static,
161    {
162        self.extend_value(name, Value::new(value))
163    }
164
165    /// Derive a child context carrying type-erased metadata.
166    pub fn extend_value(&self, name: impl Into<String>, value: Value) -> Context {
167        let mut values = (*self.meta.values).clone();
168        values.insert(name.into(), value);
169        let mut child = self.clone();
170        child.meta.values = Arc::new(values);
171        child
172    }
173
174    /// Read typed context metadata.
175    pub fn metadata<T>(&self, name: &str) -> Result<Option<Arc<T>>>
176    where
177        T: Send + Sync + 'static,
178    {
179        self.meta.values.get(name).map(Value::downcast).transpose()
180    }
181
182    /// Allocate a globally unique isolation label.
183    pub fn new_isolation(&self) -> Isolation {
184        self.root.scope()
185    }
186
187    /// Create a child context with a fresh independent service scope for
188    /// `name`.
189    pub fn isolate(&self, name: impl Into<String>) -> Context {
190        let label = self.new_isolation();
191        self.isolate_with(name, label)
192    }
193
194    /// Create a child context using a supplied scope label. Reusing the label
195    /// joins otherwise separate context branches to the same service scope.
196    ///
197    /// A scope holds one implementation per slot, and every name isolated
198    /// with the same label maps to that one slot: only one of those names
199    /// can be provided there, and providing a second fails with
200    /// [`DuplicateService`](crate::ErrorCode::DuplicateService).
201    pub fn isolate_with(&self, name: impl Into<String>, label: Isolation) -> Context {
202        let mut isolates = (*self.meta.isolates).clone();
203        isolates.insert(name.into(), label);
204        let mut child = self.clone();
205        child.meta.isolates = Arc::new(isolates);
206        child
207    }
208
209    /// Create a child context with several service names isolated together.
210    ///
211    /// All `names` share one scope label and therefore one implementation
212    /// slot, matching the upstream single-slot scope model: provide exactly
213    /// one of them in this branch — providing a second name fails with
214    /// [`DuplicateService`](crate::ErrorCode::DuplicateService) — and inject
215    /// only that name from sibling plugins. Use separate labels (or
216    /// [`isolate`](Self::isolate)) when several of the names need independent
217    /// implementations.
218    pub fn isolate_many(
219        &self,
220        names: impl IntoIterator<Item = impl Into<String>>,
221        label: Option<Isolation>,
222    ) -> Context {
223        let label = label.unwrap_or_else(|| self.new_isolation());
224        let mut child = self.clone();
225        let mut isolates = (*child.meta.isolates).clone();
226        for name in names {
227            isolates.insert(name.into(), label);
228        }
229        child.meta.isolates = Arc::new(isolates);
230        child
231    }
232
233    /// Add service-specific intercept configuration below this context.
234    pub fn intercept<T>(&self, name: impl Into<String>, config: T) -> Context
235    where
236        T: Send + Sync + 'static,
237    {
238        self.intercept_value(name, Value::new(config))
239    }
240
241    /// Add type-erased intercept configuration.
242    pub fn intercept_value(&self, name: impl Into<String>, config: Value) -> Context {
243        let mut intercepts = (*self.meta.intercepts).clone();
244        intercepts.push((name.into(), config));
245        let mut child = self.clone();
246        child.meta.intercepts = Arc::new(intercepts);
247        child
248    }
249
250    /// Return typed intercept configs in ancestor-to-descendant order.
251    pub fn intercepts<T>(&self, name: &str) -> Result<Vec<Arc<T>>>
252    where
253        T: Send + Sync + 'static,
254    {
255        self.meta
256            .intercepts
257            .iter()
258            .filter(|(entry, _)| entry == name)
259            .map(|(_, value)| value.downcast())
260            .collect()
261    }
262
263    /// Attach an event listener filter to a derived context.
264    pub fn with_filter<F>(&self, filter: F) -> Context
265    where
266        F: Fn(&Context) -> bool + Send + Sync + 'static,
267    {
268        let mut child = self.clone();
269        child.meta.filter = Some(Arc::new(filter));
270        child
271    }
272
273    /// Return the events service bound to this context.
274    pub fn events(&self) -> EventsService {
275        EventsService::new(self.clone())
276    }
277
278    /// Return the reflection/service store bound to this context.
279    pub fn reflect(&self) -> ReflectService {
280        ReflectService::new(self.clone())
281    }
282
283    /// Return the plugin registry bound to this context.
284    pub fn registry(&self) -> RegistryService {
285        RegistryService::new(self.clone())
286    }
287
288    /// Return a logger named from the current fiber and intercepts.
289    pub fn logger(&self) -> Logger {
290        LoggerService::new(self.clone()).logger(None)
291    }
292
293    /// Return an explicitly named logger.
294    pub fn named_logger(&self, name: impl Into<String>) -> Logger {
295        LoggerService::new(self.clone()).logger(Some(name.into()))
296    }
297
298    /// Return the logger service bound to this context.
299    pub fn logger_service(&self) -> LoggerService {
300        LoggerService::new(self.clone())
301    }
302
303    /// Register a synchronous cleanup operation on the current fiber.
304    pub fn effect<F>(&self, label: impl Into<String>, dispose: F) -> Result<EffectHandle>
305    where
306        F: FnOnce() -> Result<()> + Send + 'static,
307    {
308        self.fiber()?
309            .register_effect(label, AsyncDisposer::from_sync(dispose))
310    }
311
312    /// Register an infallible synchronous cleanup operation.
313    pub fn effect_infallible<F>(&self, label: impl Into<String>, dispose: F) -> Result<EffectHandle>
314    where
315        F: FnOnce() + Send + 'static,
316    {
317        self.fiber()?
318            .register_effect(label, AsyncDisposer::infallible(dispose))
319    }
320
321    /// Register an asynchronous cleanup operation.
322    pub fn effect_async<F, Fut>(&self, label: impl Into<String>, dispose: F) -> Result<EffectHandle>
323    where
324        F: FnOnce() -> Fut + Send + 'static,
325        Fut: Future<Output = Result<()>> + Send + 'static,
326    {
327        self.fiber()?
328            .register_effect(label, AsyncDisposer::from_async(dispose))
329    }
330
331    /// Provide a concrete service in this context's isolation scope.
332    pub fn provide<T>(&self, name: impl Into<String>, value: T) -> Result<EffectHandle>
333    where
334        T: Send + Sync + 'static,
335    {
336        self.reflect()
337            .provide_value(name.into(), Value::new(value), None)
338    }
339
340    /// Provide an existing `Arc` without wrapping it in a second `Arc`.
341    pub fn provide_arc<T>(&self, name: impl Into<String>, value: Arc<T>) -> Result<EffectHandle>
342    where
343        T: Send + Sync + 'static,
344    {
345        self.reflect()
346            .provide_value(name.into(), Value::from_arc(value), None)
347    }
348
349    /// Read a service without enforcing an inject declaration.
350    pub fn get<T>(&self, name: &str) -> Result<Option<Arc<T>>>
351    where
352        T: Send + Sync + 'static,
353    {
354        self.reflect().get(name, true)
355    }
356
357    /// Read a service even while its provider is loading or unloading.
358    pub fn get_unchecked<T>(&self, name: &str) -> Result<Option<Arc<T>>>
359    where
360        T: Send + Sync + 'static,
361    {
362        self.reflect().get(name, false)
363    }
364
365    /// Require a currently active service.
366    pub fn require<T>(&self, name: &str) -> Result<Arc<T>>
367    where
368        T: Send + Sync + 'static,
369    {
370        self.reflect().require(name)
371    }
372
373    /// Replace a service value. Only its providing fiber may do this.
374    ///
375    /// The replacement does not wake dependent fibers; see
376    /// [`ReflectService::set_value`](crate::ReflectService::set_value) and
377    /// [`notify`](Self::notify).
378    pub fn set<T>(&self, name: &str, value: T) -> Result<()>
379    where
380        T: Send + Sync + 'static,
381    {
382        self.reflect().set_value(name, Value::new(value))
383    }
384
385    /// Re-evaluate dependency availability for the named services.
386    pub fn notify<I, S>(&self, names: I) -> Vec<Fiber>
387    where
388        I: IntoIterator<Item = S>,
389        S: AsRef<str>,
390    {
391        self.reflect().notify(names)
392    }
393
394    /// Register a dynamic computed property.
395    pub fn accessor(&self, name: impl Into<String>, accessor: Accessor) -> Result<EffectHandle> {
396        self.reflect().accessor(name.into(), accessor)
397    }
398
399    /// Register an event listener owned by this context's fiber.
400    pub fn on<F>(&self, name: impl Into<String>, listener: F) -> Result<EffectHandle>
401    where
402        F: Fn(Event) -> EventResult + Send + Sync + 'static,
403    {
404        self.events().on(name, listener, EventOptions::default())
405    }
406
407    /// Register a listener with placement and filtering options.
408    pub fn on_with<F>(
409        &self,
410        name: impl Into<String>,
411        listener: F,
412        options: EventOptions,
413    ) -> Result<EffectHandle>
414    where
415        F: Fn(Event) -> EventResult + Send + Sync + 'static,
416    {
417        self.events().on(name, listener, options)
418    }
419
420    /// Register an asynchronous event listener.
421    ///
422    /// See [`EventsService::on_async`](crate::EventsService::on_async) for the
423    /// blocking-executor caveats before awaiting thread-local work here.
424    pub fn on_async<F, Fut>(&self, name: impl Into<String>, listener: F) -> Result<EffectHandle>
425    where
426        F: Fn(Event) -> Fut + Send + Sync + 'static,
427        Fut: Future<Output = EventResult> + Send + 'static,
428    {
429        self.events()
430            .on_async(name, listener, EventOptions::default())
431    }
432
433    /// Register a one-shot event listener.
434    pub fn once<F>(&self, name: impl Into<String>, listener: F) -> Result<EffectHandle>
435    where
436        F: Fn(Event) -> EventResult + Send + Sync + 'static,
437    {
438        self.events().once(name, listener, EventOptions::default())
439    }
440
441    /// Emit an event synchronously.
442    pub fn emit(
443        &self,
444        name: impl Into<String>,
445        args: impl IntoIterator<Item = EventValue>,
446    ) -> Result<()> {
447        self.events().emit(name, args)
448    }
449
450    /// Run all matching listeners concurrently.
451    pub async fn parallel(
452        &self,
453        name: impl Into<String>,
454        args: impl IntoIterator<Item = EventValue>,
455    ) -> Result<()> {
456        self.events().parallel(name, args).await
457    }
458
459    /// Await listeners in order until one returns a bail value.
460    pub async fn serial(
461        &self,
462        name: impl Into<String>,
463        args: impl IntoIterator<Item = EventValue>,
464    ) -> EventResult {
465        self.events().serial(name, args).await
466    }
467
468    /// Run listeners synchronously until one returns a bail value.
469    pub fn bail(
470        &self,
471        name: impl Into<String>,
472        args: impl IntoIterator<Item = EventValue>,
473    ) -> EventResult {
474        self.events().bail(name, args)
475    }
476
477    /// Compose listeners around an innermost synchronous callback.
478    pub fn waterfall<F>(
479        &self,
480        name: impl Into<String>,
481        args: impl IntoIterator<Item = EventValue>,
482        inner: F,
483    ) -> EventResult
484    where
485        F: Fn() -> EventResult + Send + Sync + 'static,
486    {
487        self.events().waterfall(name, args, inner)
488    }
489
490    /// Start a plugin and return its lifecycle fiber.
491    pub fn plugin<P, C>(&self, plugin: P, config: C) -> Fiber
492    where
493        P: IntoPlugin,
494        C: Send + Sync + 'static,
495    {
496        self.registry()
497            .plugin_value(plugin.into_plugin(), Config::new(config))
498    }
499
500    /// Start a plugin with unit configuration.
501    pub fn plugin_default<P>(&self, plugin: P) -> Fiber
502    where
503        P: IntoPlugin,
504    {
505        self.registry()
506            .plugin_value(plugin.into_plugin(), Config::default())
507    }
508
509    /// Wrap and start a concrete [`Plugin`](crate::Plugin) implementation.
510    pub fn plugin_object<P, C>(&self, plugin: P, config: C) -> Fiber
511    where
512        P: crate::Plugin,
513        C: Send + Sync + 'static,
514    {
515        self.registry()
516            .plugin_value(crate::PluginHandle::new(plugin), Config::new(config))
517    }
518
519    /// Start a concrete plugin implementation with unit configuration.
520    pub fn plugin_object_default<P>(&self, plugin: P) -> Fiber
521    where
522        P: crate::Plugin,
523    {
524        self.registry()
525            .plugin_value(crate::PluginHandle::new(plugin), Config::default())
526    }
527
528    /// Start an inline callback after all listed services become available.
529    pub fn inject<F>(&self, inject: Inject, callback: F) -> Fiber
530    where
531        F: Fn(Context) -> Result<PluginOutput> + Send + Sync + 'static,
532    {
533        let plugin = crate::plugin_sync::<(), _>("anonymous", inject, move |ctx, _| callback(ctx));
534        self.plugin_default(plugin)
535    }
536
537    /// Log an error through the current context logger.
538    pub fn log_error(&self, error: impl ToString) {
539        self.logger().error(error.to_string(), Vec::<LogArg>::new());
540    }
541
542    pub(crate) fn scope_override(&self, name: &str) -> Option<Isolation> {
543        self.meta.isolates.get(name).copied()
544    }
545
546    pub(crate) fn filter(&self) -> Option<&ContextFilter> {
547        self.meta.filter.as_ref()
548    }
549
550    pub(crate) fn root_arc(&self) -> &Arc<RootInner> {
551        &self.root
552    }
553}
554
555impl Default for Context {
556    fn default() -> Self {
557        Self::new()
558    }
559}
560
561impl Debug for Context {
562    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
563        let name = self
564            .fiber()
565            .map(|fiber| fiber.name())
566            .unwrap_or_else(|_| "disposed".to_owned());
567        f.debug_tuple("Context").field(&name).finish()
568    }
569}