Skip to main content

cordis/
context.rs

1use parking_lot::RwLock;
2use std::any::{Any, TypeId};
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicUsize, Ordering};
6use std::sync::{Arc, Weak};
7
8thread_local! {
9    static ACTIVE_PROVIDER_FIBERS: RefCell<Vec<(usize, Arc<Fiber>)>> = const { RefCell::new(Vec::new()) };
10}
11
12use crate::fiber::{Fiber, FiberState};
13use crate::registry::{Plugin, RegistryService};
14use crate::service::{CordisError, Service};
15use crate::{FiberId, ReflectService, Symbol};
16
17pub(crate) static NEXT_FIBER_ID: AtomicUsize = AtomicUsize::new(1);
18
19/// Erased getter for one name-keyed accessor. Receives the context so a
20/// computed property can compose over services; MUST NOT consult the
21/// `internal/get` waterfall (accessor reads bypass interception by design).
22pub type AccessorGetter =
23    Arc<dyn Fn(&Context) -> Result<Option<Arc<dyn Any + Send + Sync>>, CordisError> + Send + Sync>;
24
25/// Erased setter for one name-keyed accessor. Bypasses the `internal/set`
26/// waterfall by construction.
27pub type AccessorSetter =
28    Arc<dyn Fn(&Context, Arc<dyn Any + Send + Sync>) -> Result<(), CordisError> + Send + Sync>;
29
30/// Declarative descriptor handed to [`Context::register_accessor`].
31#[derive(Clone, Default)]
32pub struct Accessor {
33    getter: Option<AccessorGetter>,
34    setter: Option<AccessorSetter>,
35}
36
37impl Accessor {
38    /// Readable property: reads resolve through `getter`, writes are refused
39    /// with [`CordisError::ReadOnlyProperty`].
40    pub fn read_only<F>(getter: F) -> Self
41    where
42        F: Fn(&Context) -> Result<Option<Arc<dyn Any + Send + Sync>>, CordisError>
43            + Send
44            + Sync
45            + 'static,
46    {
47        Self { getter: Some(Arc::new(getter)), setter: None }
48    }
49
50    /// Read-write property.
51    pub fn read_write<G, S>(getter: G, setter: S) -> Self
52    where
53        G: Fn(&Context) -> Result<Option<Arc<dyn Any + Send + Sync>>, CordisError>
54            + Send
55            + Sync
56            + 'static,
57        S: Fn(&Context, Arc<dyn Any + Send + Sync>) -> Result<(), CordisError> + Send + Sync + 'static,
58    {
59        Self { getter: Some(Arc::new(getter)), setter: Some(Arc::new(setter)) }
60    }
61
62    /// Write-only property: reads resolve `None`, writes go through `setter`.
63    pub fn setter_only<S>(setter: S) -> Self
64    where
65        S: Fn(&Context, Arc<dyn Any + Send + Sync>) -> Result<(), CordisError> + Send + Sync + 'static,
66    {
67        Self { getter: None, setter: Some(Arc::new(setter)) }
68    }
69}
70
71/// Shared registration record: one declaration plus every name bound to it
72/// (the declared name and any [`Context::alias`] alternates). Disposing the
73/// handle removes ALL bound names in one shot.
74struct AccessorSlot {
75    getter: Option<AccessorGetter>,
76    setter: Option<AccessorSetter>,
77    names: parking_lot::Mutex<Vec<String>>,
78}
79
80/// Handle returned by [`Context::register_accessor`]. Call
81/// [`EffectHandle::dispose`] to remove the accessor (and its aliases).
82pub struct EffectHandle {
83    ctx: Weak<Context>,
84    slot: Weak<AccessorSlot>,
85}
86
87impl EffectHandle {
88    /// Remove the registered accessor and every alias pointing at it.
89    /// Returns `true` when the declaration was still live and got removed.
90    pub fn dispose(self) -> bool {
91        let (Some(ctx), Some(slot)) = (self.ctx.upgrade(), self.slot.upgrade()) else {
92            return false;
93        };
94        let names = slot.names.lock().clone();
95        let mut accessors = ctx.accessors.write();
96        let mut removed = false;
97        for name in names {
98            if accessors
99                .get(&name)
100                .is_some_and(|bound| std::sync::Arc::ptr_eq(bound, &slot))
101            {
102                accessors.remove(&name);
103                removed = true;
104            }
105        }
106        removed
107    }
108}
109
110pub struct Context {
111    store: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>,
112    isolate: RwLock<HashMap<TypeId, Symbol>>,
113    /// Layered intercept overrides per TypeId, ordered OUTERMOST..INNERMOST.
114    /// Every set APPENDS a layer; the effective value is the innermost (the
115    /// last element). See [`Context::intercept_chain`].
116    intercept: RwLock<HashMap<TypeId, Vec<Arc<dyn Any + Send + Sync>>>>,
117    /// Name-keyed computed-property accessors living beside the TypeId
118    /// service store. Accessor reads/writes deliberately bypass the
119    /// `internal/get` / `internal/set` intercept waterfalls.
120    accessors: RwLock<HashMap<String, std::sync::Arc<AccessorSlot>>>,
121    versions: RwLock<HashMap<TypeId, u64>>,
122    /// Semantic peer-dependency versions declared alongside a value by
123    /// [`Context::provide_versioned`]. Legacy `provide` paths keep this map
124    /// empty, so an absent entry reads as provider version 0.
125    provided_versions: RwLock<HashMap<TypeId, u64>>,
126    // Providers installed by a registration fiber are hidden while that fiber
127    // is inactive, reloading, or failed. Direct `provide` values remain
128    // permissive for existing root/test APIs.
129    owners: RwLock<HashMap<TypeId, Weak<Fiber>>>,
130    fiber: Arc<Fiber>,
131    parent: Option<Arc<Context>>,
132    root: Weak<Context>,
133}
134
135impl Context {
136    pub(crate) fn with_provider_fiber<R>(
137        self: &Arc<Self>,
138        fiber: &Arc<Fiber>,
139        f: impl FnOnce() -> R,
140    ) -> R {
141        let key = Arc::as_ptr(self) as usize;
142        ACTIVE_PROVIDER_FIBERS.with(|stack| stack.borrow_mut().push((key, fiber.clone())));
143        struct Scope;
144        impl Drop for Scope {
145            fn drop(&mut self) {
146                ACTIVE_PROVIDER_FIBERS.with(|stack| {
147                    let _ = stack.borrow_mut().pop();
148                });
149            }
150        }
151        let _scope = Scope;
152        f()
153    }
154
155    fn active_provider_fiber(&self) -> Option<Arc<Fiber>> {
156        let key = self as *const Context as usize;
157        ACTIVE_PROVIDER_FIBERS.with(|stack| {
158            stack
159                .borrow()
160                .iter()
161                .rev()
162                .find(|(context, _)| *context == key)
163                .map(|(_, fiber)| fiber.clone())
164        })
165    }
166    pub fn new_root() -> Arc<Self> {
167        Arc::new_cyclic(|weak| Self {
168            store: RwLock::new(HashMap::new()),
169            isolate: RwLock::new(HashMap::new()),
170            intercept: RwLock::new(HashMap::new()),
171            accessors: RwLock::new(HashMap::new()),
172            versions: RwLock::new(HashMap::new()),
173            provided_versions: RwLock::new(HashMap::new()),
174            owners: RwLock::new(HashMap::new()),
175            fiber: Arc::new(Fiber::new()),
176            parent: None,
177            root: weak.clone(),
178        })
179    }
180
181    pub fn extend(self: &Arc<Self>) -> Arc<Self> {
182        Arc::new(Self {
183            store: RwLock::new(HashMap::new()),
184            isolate: RwLock::new(HashMap::new()),
185            intercept: RwLock::new(HashMap::new()),
186            accessors: RwLock::new(HashMap::new()),
187            versions: RwLock::new(HashMap::new()),
188            provided_versions: RwLock::new(HashMap::new()),
189            owners: RwLock::new(HashMap::new()),
190            fiber: Arc::new(Fiber::new()),
191            parent: Some(self.clone()),
192            root: self.root.clone(),
193        })
194    }
195
196    pub fn isolate_type(self: &Arc<Self>, tid: TypeId, label: impl Into<Symbol>) -> Arc<Self> {
197        let mut parent_isolate = self.isolate.read().clone();
198        parent_isolate.insert(tid, label.into());
199        Arc::new(Self {
200            store: RwLock::new(HashMap::new()),
201            isolate: RwLock::new(parent_isolate),
202            intercept: RwLock::new(HashMap::new()),
203            accessors: RwLock::new(HashMap::new()),
204            versions: RwLock::new(HashMap::new()),
205            provided_versions: RwLock::new(HashMap::new()),
206            owners: RwLock::new(HashMap::new()),
207            fiber: Arc::new(Fiber::new()),
208            parent: Some(self.clone()),
209            root: self.root.clone(),
210        })
211    }
212
213    pub fn isolate<T: Service>(self: &Arc<Self>, label: impl Into<Symbol>) -> Arc<Self> {
214        self.isolate_type(TypeId::of::<T>(), label)
215    }
216
217    pub fn intercept<T: Service>(self: &Arc<Self>, val: T) -> Arc<Self> {
218        let child = self.extend();
219        let tid = TypeId::of::<T>();
220        let any: Arc<dyn Any + Send + Sync> = Arc::new(val);
221        // Append a fresh layer on the forked frame; effective stays innermost.
222        child.intercept.write().entry(tid).or_default().push(any);
223        // bump version for intercept as well? Not needed for epoch but keep
224        child
225    }
226
227    /// Semantic peer-dependency versioning scheme (paper §open-problems,
228    /// peer dependencies).
229    ///
230    /// A provider's version is a plain `u64`. The **major** component lives in
231    /// the high bits: `major(v) = v / 100_000`, and the **minimum compatible
232    /// floor** is the remainder `v % 100_000` within that major. An inject
233    /// constrained with `requirement = M * 100_000 + f` is satisfied by a
234    /// provider of version `p` if and only if
235    ///
236    /// * the provider exists and is available, and
237    /// * `major(p) == major(requirement)` (same-major compatibility — peer
238    ///   dependencies never bind across a breaking boundary), and
239    /// * `p >= requirement` (the provider is at least the requested floor;
240    ///   under equal majors this is exactly "remainder >= floor").
241    ///
242    /// Any mismatch leaves the inject **unsatisfied**: the dependent fiber
243    /// goes, and stays, `Inactive` rather than silently binding a wrong
244    /// version. Providers installed through legacy [`Self::provide`] carry
245    /// version 0, so they satisfy only unconstrained injects; migrate them to
246    /// [`Self::provide_versioned`] to opt into constraint matching.
247    pub const VERSION_MAJOR_SCALE: u64 = 100_000;
248
249    /// Provide `value` under `T` together with a semantic peer-dependency
250    /// version. See the [`Self::VERSION_MAJOR_SCALE`] documentation for the
251    /// exact satisfaction scheme. Ownership/undo semantics are identical to
252    /// [`Self::provide`].
253    pub fn provide_versioned<T: Any + Send + Sync>(
254        self: &Arc<Self>,
255        value: T,
256        version: u64,
257    ) -> Arc<T> {
258        let owner = self.active_provider_fiber();
259        self.provide_impl(Arc::new(value), owner.as_ref(), Some(version))
260    }
261
262    /// The semantic peer-dependency version recorded for `tid`, walking the
263    /// parent chain like [`Self::get_version`]. Returns 0 when no value was
264    /// provided or when it was installed through an unversioned path.
265    ///
266    /// Distinct from [`Self::get_version`], which counts structural store
267    /// mutations of `tid` and drives epoch strings; this reads the declared
268    /// compatibility contract used by version-constrained injects.
269    pub fn provider_version(&self, tid: TypeId) -> u64 {
270        if let Some(v) = self.provided_versions.read().get(&tid) {
271            return *v;
272        }
273        if let Some(parent) = &self.parent {
274            return parent.provider_version(tid);
275        }
276        0
277    }
278
279    // Direct providers are owned by this context's fiber for compatibility.
280    pub fn provide<T: Service>(self: &Arc<Self>, svc: T) -> Arc<T> {
281        let owner = self.active_provider_fiber();
282        self.provide_impl(Arc::new(svc), owner.as_ref(), None)
283    }
284
285    /// Install a provider and record its undo on an explicit registration
286    /// fiber. This is used by RegistryService so disposing one registration
287    /// cannot remove another registration's service.
288    pub(crate) fn provide_on_fiber<T: Service>(
289        self: &Arc<Self>,
290        svc: Arc<T>,
291        owner: &Arc<Fiber>,
292    ) -> Arc<T> {
293        self.provide_impl(svc, Some(owner), None)
294    }
295
296    /// Install a service value in this context's store.
297    ///
298    /// `semantic_version` carries the peer-dependency contract from
299    /// [`Context::provide_versioned`]: `Some(v)` records `v` in the
300    /// [`Self::provided_versions`] map (visible to [`Self::provider_version`]
301    /// and to inject satisfaction checks), `None` clears any entry so legacy
302    /// `provide` paths read as provider version 0. This is independent of the
303    /// structural mutation counter in `versions`, which keeps ticking on every
304    /// store change.
305    fn provide_impl<T: Any + Send + Sync>(
306        self: &Arc<Self>,
307        svc: Arc<T>,
308        owner: Option<&Arc<Fiber>>,
309        semantic_version: Option<u64>,
310    ) -> Arc<T> {
311        let tid = TypeId::of::<T>();
312        // C1 `internal/set` write veto: a failing chain refuses THIS write
313        // by returning the PREVIOUS value for `tid` (the old binding stays
314        // fully intact — no store/owners/version mutation happens). With no
315        // listener registered the consult is two map reads.
316        if let Some(events) = self.get_unintercepted::<crate::EventsService>() {
317            if crate::events::blocking_intercept_set(&events, std::any::type_name::<T>()).is_err()
318            {
319                tracing::info!(
320                    service = std::any::type_name::<T>(),
321                    "internal/set vetoed provider write; previous value stays"
322                );
323                let prev_any = self.store.read().get(&tid).cloned();
324                return prev_any.and_then(|any| any.downcast::<T>().ok()).unwrap_or(svc);
325            }
326        }
327        let any: Arc<dyn Any + Send + Sync> = svc.clone();
328        let prev = self.store.write().insert(tid, any.clone());
329        if tid == TypeId::of::<ReflectService>() {
330            if let Ok(reflect) = any.downcast::<ReflectService>() {
331                reflect.set_context(self);
332            }
333        }
334        let prev_owner = if let Some(owner) = owner {
335            self.owners.write().insert(tid, Arc::downgrade(owner))
336        } else {
337            self.owners.write().remove(&tid)
338        };
339        {
340            let mut versions = self.versions.write();
341            *versions.entry(tid).or_insert(0) += 1;
342        }
343        let prev_semantic = match semantic_version {
344            Some(v) => self.provided_versions.write().insert(tid, v),
345            None => self.provided_versions.write().remove(&tid),
346        };
347        if let Some(reflect) = self.get::<ReflectService>() {
348            reflect.notify(tid);
349        }
350        let weak = Arc::downgrade(self);
351        let undo: Box<dyn FnOnce() + Send> = Box::new(move || {
352            if let Some(ctx) = weak.upgrade() {
353                // Release all context write guards before looking up ReflectService.
354                // Context get reads the store; calling it while the store write lock is held
355                // deadlocks parking_lot's non-reentrant RwLock during disposal.
356                {
357                    let mut store = ctx.store.write();
358                    if let Some(prev_any) = prev {
359                        store.insert(tid, prev_any);
360                    } else {
361                        store.remove(&tid);
362                    }
363                    let mut owners = ctx.owners.write();
364                    if let Some(previous) = prev_owner {
365                        owners.insert(tid, previous);
366                    } else {
367                        owners.remove(&tid);
368                    }
369                    let mut versions = ctx.versions.write();
370                    if let Some(v) = versions.get_mut(&tid) {
371                        *v = v.saturating_sub(1);
372                        if *v == 0 {
373                            versions.remove(&tid);
374                        }
375                    }
376                    // Restore the semantic peer-version entry the provide
377                    // replaced (or clear it when none existed), mirroring the
378                    // store/owners LIFO discipline.
379                    match prev_semantic {
380                        Some(v) => {
381                            ctx.provided_versions.write().insert(tid, v);
382                        }
383                        None => {
384                            ctx.provided_versions.write().remove(&tid);
385                        }
386                    }
387                }
388                if let Some(reflect) = ctx.get::<ReflectService>() {
389                    reflect.notify(tid);
390                }
391            }
392        });
393        owner
394            .cloned()
395            .unwrap_or_else(|| self.fiber.clone())
396            .push_undo(undo);
397        svc
398    }
399
400    /// Remove a service from the store and trigger deactivation cascade.
401    ///
402    /// Guarded withdrawal: when a [`RegistryService`] is present and active
403    /// consumer fibers still resolve `T` in this isolate realm, removal is
404    /// refused with a `guarded withdrawal` configuration error instead of
405    /// pulling the dependency out from under them. Internal rollback paths
406    /// (fiber undo stacks) bypass the guard via [`Self::remove_forced`].
407    /// Pushes the inverse (re-provide) onto the fiber's accumulator for LIFO
408    /// reversal once the guard permits the removal.
409    pub fn remove<T: Service>(self: &Arc<Self>) -> Result<Option<Arc<T>>, CordisError> {
410        let tid = TypeId::of::<T>();
411        if self.store.read().contains_key(&tid) {
412            if let Some(registry) = self.get::<RegistryService>() {
413                let key = (tid, self.isolate_label(tid));
414                let consumers = registry.reliance_count(&key);
415                if consumers > 0 {
416                    return Err(CordisError::Configuration(format!(
417                        "guarded withdrawal: {consumers} active consumer(s) still rely on {}",
418                        std::any::type_name::<T>()
419                    )));
420                }
421            }
422        }
423        Self::remove_forced::<T>(self)
424    }
425
426    /// Unconditional removal -- the rollback primitive behind [`Self::remove`].
427    /// Internal undo paths must never be blocked by the guarded-withdrawal
428    /// check, so they call this directly.
429    pub(crate) fn remove_forced<T: Service>(
430        self: &Arc<Self>,
431    ) -> Result<Option<Arc<T>>, CordisError> {
432        let tid = TypeId::of::<T>();
433        let removed = {
434            let mut store = self.store.write();
435            store.remove(&tid)
436        };
437        if let Some(any) = removed {
438            let previous_owner = self.owners.write().remove(&tid);
439            // Adjust version down (or remove entirely)
440            {
441                let mut versions = self.versions.write();
442                if let Some(v) = versions.get_mut(&tid) {
443                    *v = v.saturating_sub(1);
444                    if *v == 0 {
445                        versions.remove(&tid);
446                    }
447                }
448            }
449            // Notify dependents to deactivate
450            if let Some(reflect) = self.get::<ReflectService>() {
451                reflect.notify(tid);
452            }
453            // Push undo (re-provide) for LIFO reversal
454            let weak = Arc::downgrade(self);
455            let fiber = self
456                .active_provider_fiber()
457                .unwrap_or_else(|| self.fiber.clone());
458            let any_clone = any.clone();
459            let undo: Box<dyn FnOnce() + Send> = Box::new(move || {
460                if let Some(ctx) = weak.upgrade() {
461                    ctx.store.write().insert(tid, any_clone);
462                    if let Some(owner) = previous_owner {
463                        ctx.owners.write().insert(tid, owner);
464                    }
465                    let mut versions = ctx.versions.write();
466                    let e = versions.entry(tid).or_insert(0);
467                    *e += 1;
468                }
469            });
470            fiber.push_undo(undo);
471            // Downcast to the concrete type
472            Ok(any.downcast::<T>().ok())
473        } else {
474            Ok(None)
475        }
476    }
477
478    /// Install an already-built service under a concrete `TypeId` without
479    /// registering it with the [`RegistryService`].
480    ///
481    /// Used by the loader's verified hot-swap trial: the candidate fiber is
482    /// applied out-of-band first so a failing factory cannot kill the live
483    /// provider. Returns `Err(tid)` when the slot is already occupied.
484    pub(crate) fn provide_untyped(
485        self: &Arc<Self>,
486        tid: TypeId,
487        any: Arc<dyn Any + Send + Sync>,
488    ) -> Result<(), TypeId> {
489        let mut store = self.store.write();
490        if store.contains_key(&tid) {
491            return Err(tid);
492        }
493        if tid == TypeId::of::<ReflectService>() {
494            if let Ok(reflect) = any.clone().downcast::<ReflectService>() {
495                reflect.set_context(self);
496            }
497        }
498        store.insert(tid, any);
499        Ok(())
500    }
501
502    /// Take the raw value stored for `tid` without notifying dependents or
503    /// touching versions/owners. Companion to [`Self::provide_untyped`] for
504    /// the verified hot-swap trial teardown.
505    pub(crate) fn take_untyped(&self, tid: TypeId) -> Option<Arc<dyn Any + Send + Sync>> {
506        self.store.write().remove(&tid)
507    }
508
509    /// Untyped availability probe mirroring `get::<T>()` semantics without a
510    /// generic parameter (the trial's `Provides` type is erased).
511    pub(crate) fn get_untyped(&self, tid: TypeId) -> Option<Arc<dyn Any + Send + Sync>> {
512        self.store.read().get(&tid).cloned()
513    }
514
515    /// Install an untyped intercept override without forking a child context.
516    /// Companion to [`Self::bind_intercept`] for erased service values.
517    pub(crate) fn bind_intercept_untyped(&self, tid: TypeId, any: Arc<dyn Any + Send + Sync>) {
518        // Layered: appending keeps the previous outer layers intact and makes
519        // `any` the new effective (innermost) value.
520        self.intercept.write().entry(tid).or_default().push(any);
521    }
522
523    /// Read the EFFECTIVE (innermost) intercept override without removing it.
524    pub(crate) fn peek_intercept_untyped(&self, tid: TypeId) -> Option<Arc<dyn Any + Send + Sync>> {
525        self.intercept.read().get(&tid).and_then(|layers| layers.last()).cloned()
526    }
527
528    /// Pop the innermost intercept layer. The key is removed entirely once
529    /// the last layer goes.
530    pub(crate) fn remove_intercept_untyped(&self, tid: TypeId) {
531        let mut intercept = self.intercept.write();
532        if let Some(layers) = intercept.get_mut(&tid) {
533            layers.pop();
534            if layers.is_empty() {
535                intercept.remove(&tid);
536            }
537        }
538    }
539
540    /// Relaxed read: like [`Self::get`], but a locally-owned provider whose
541    /// owner fiber rests in a TRANSITIONING state (`Loading`, `Reloading`,
542    /// `Unloading`, or reactive `Pending`) still resolves. Strict [`Self::get`]
543    /// refuses those so consumers never observe mid-transition values;
544    /// lifecycle/observer code (and tests) use this to inspect the value that
545    /// is about to serve or was just retracted during transitions.
546    ///
547    /// Terminal resting states (`Failed`, disposed) and missing owners stay
548    /// refused exactly as in [`Self::get`].
549    pub fn get_relaxed<T: Service>(&self) -> Option<Arc<T>> {
550        let tid = TypeId::of::<T>();
551        if self.isolate_label(tid).is_none() {
552            if let Some(any) = self.intercept.read().get(&tid).and_then(|l| l.last()) {
553                if let Ok(arc) = any.clone().downcast::<T>() {
554                    return Some(arc);
555                }
556            }
557        }
558        if let Some(any) = self.store.read().get(&tid) {
559            let transitioning = self
560                .owners
561                .read()
562                .get(&tid)
563                .and_then(Weak::upgrade)
564                .map(|fiber| {
565                    matches!(
566                        fiber.state(),
567                        FiberState::Active { .. }
568                            | FiberState::Loading
569                            | FiberState::Reloading
570                            | FiberState::Unloading { .. }
571                            | FiberState::Pending
572                    )
573                })
574                .unwrap_or(true);
575            // Disposed fibers stay refused even in relaxed mode: disposal
576            // already ran its undos, so the value is logically gone.
577            if transitioning && !self.disposed_owner(tid) {
578                if let Ok(arc) = any.clone().downcast::<T>() {
579                    return Some(arc);
580                }
581            }
582            return None;
583        }
584        if self.isolate.read().contains_key(&tid) {
585            return self.parent.as_ref().and_then(|parent| {
586                if parent.isolate_label(tid) == self.isolate_label(tid) {
587                    parent.get_relaxed::<T>()
588                } else {
589                    None
590                }
591            });
592        }
593        self.parent.as_ref().and_then(|parent| parent.get_relaxed::<T>())
594    }
595
596    /// True when the owner fiber of `tid` has been disposed.
597    fn disposed_owner(&self, tid: TypeId) -> bool {
598        self.owners
599            .read()
600            .get(&tid)
601            .and_then(Weak::upgrade)
602            .map(|fiber| fiber.is_disposed())
603            .unwrap_or(false)
604    }
605
606    pub fn get<T: Service>(&self) -> Option<Arc<T>> {
607        // C1 `internal/get` strict-read interception: consult the veto chain
608        // ONCE per top-level read. Redirect skips this frame's bindings so a
609        // parent binding serves the read; Refuse fails the lookup outright.
610        if let Some(events) = self.get_unintercepted::<crate::EventsService>() {
611            match crate::events::blocking_intercept_get(&events, std::any::type_name::<T>()) {
612                crate::events::ReadVerdict::Pass => {}
613                crate::events::ReadVerdict::RedirectFrame => {
614                    return self.get_from_parent_frame::<T>();
615                }
616                crate::events::ReadVerdict::Refuse => return None,
617            }
618        }
619        self.get_impl::<T>()
620    }
621
622    /// Internal read used by the interception bridges themselves: resolves a
623    /// service WITHOUT re-consulting `internal/get` (the thread-local fence
624    /// already guarantees no recursion; this path additionally avoids the
625    /// bridge call for kernel-internal lookups).
626    fn get_unintercepted<T: Service>(&self) -> Option<Arc<T>> {
627        self.get_impl::<T>()
628    }
629
630    /// Continue a redirected strict read at the PARENT frame, skipping this
631    /// context's store + intercept bindings entirely. The root frame answers
632    /// `None` — there is nothing above to serve from.
633    fn get_from_parent_frame<T: Service>(&self) -> Option<Arc<T>> {
634        let Some(parent) = &self.parent else {
635            return None;
636        };
637        if parent.isolate_label(TypeId::of::<T>()) != self.isolate_label(TypeId::of::<T>()) {
638            return None;
639        }
640        // The parent lookup runs under the same fence as the original read,
641        // so it will not re-consult the veto chain.
642        parent.get_impl::<T>()
643    }
644
645    /// The historical strict-read body shared by [`Self::get`] and the two
646    /// helpers above.
647    fn get_impl<T: Service>(&self) -> Option<Arc<T>> {
648        let tid = TypeId::of::<T>();
649        // Isolate-labeled TypeIds resolve from store / isolate parent walk.
650        // Unlabeled TypeIds still let the EFFECTIVE (innermost) intercept win.
651        if self.isolate_label(tid).is_none() {
652            if let Some(any) = self.intercept.read().get(&tid).and_then(|l| l.last()) {
653                if let Ok(arc) = any.clone().downcast::<T>() {
654                    return Some(arc);
655                }
656            }
657        }
658        let mut local_provider = false;
659        if let Some(any) = self.store.read().get(&tid) {
660            local_provider = true;
661            let active = self
662                .owners
663                .read()
664                .get(&tid)
665                .and_then(Weak::upgrade)
666                .map(|fiber| matches!(fiber.state(), FiberState::Active { .. }))
667                .unwrap_or(true);
668            if active {
669                if let Ok(arc) = any.clone().downcast::<T>() {
670                    if arc.check() {
671                        return Some(arc);
672                    }
673                }
674            }
675        }
676        if local_provider {
677            return None;
678        }
679        // An isolate label is a realm boundary. A provider from an unlabeled
680        // parent must not leak into a labeled child, nor may another label
681        // satisfy this lookup. Unrelated service types still walk normally.
682        if self.isolate.read().contains_key(&tid) {
683            return self.parent.as_ref().and_then(|parent| {
684                if parent.isolate_label(tid) == self.isolate_label(tid) {
685                    parent.get_impl::<T>()
686                } else {
687                    None
688                }
689            });
690        }
691        self.parent.as_ref().and_then(|parent| parent.get_impl::<T>())
692    }
693
694    /// The single-source-discipline refusal for a TypeId that is already
695    /// provided in the same isolate realm. Uses the structured
696    /// [`CordisError::DuplicateProvider`] variant; its Display keeps the
697    /// `duplicate provider` phrase that tests and docs assert on.
698    fn duplicate_provider_error(tid: TypeId) -> CordisError {
699        CordisError::DuplicateProvider {
700            name: format!("{tid:?}"),
701            owner: "context".to_string(),
702        }
703    }
704
705    pub fn get_version(&self, tid: TypeId) -> u64 {
706        if let Some(v) = self.versions.read().get(&tid) {
707            return *v;
708        }
709        if let Some(parent) = &self.parent {
710            return parent.get_version(tid);
711        }
712        0
713    }
714
715    pub(crate) fn is_available(&self, tid: TypeId) -> bool {
716        if self
717            .isolate_label(tid)
718            .is_none()
719            && self.intercept.read().get(&tid).is_some_and(|layers| !layers.is_empty())
720        {
721            return true;
722        }
723        if self.store.read().contains_key(&tid) {
724            return self
725                .owners
726                .read()
727                .get(&tid)
728                .and_then(Weak::upgrade)
729                .map(|fiber| matches!(fiber.state(), FiberState::Active { .. }))
730                .unwrap_or(true);
731        }
732        if self.isolate.read().contains_key(&tid) {
733            return self.parent.as_ref().is_some_and(|parent| {
734                parent.isolate_label(tid) == self.isolate_label(tid) && parent.is_available(tid)
735            });
736        }
737        self.parent
738            .as_ref()
739            .is_some_and(|parent| parent.is_available(tid))
740    }
741
742    pub fn isolate_label(&self, tid: TypeId) -> Option<Symbol> {
743        if let Some(label) = self.isolate.read().get(&tid).cloned() {
744            return Some(label);
745        }
746        if let Some(parent) = &self.parent {
747            return parent.isolate_label(tid);
748        }
749        None
750    }
751
752    /// TypeIds currently provided in this context's store (not parent/intercept).
753    pub fn provided_type_ids(&self) -> Vec<TypeId> {
754        self.store.read().keys().copied().collect()
755    }
756
757    /// Record an isolate namespace on this context without forking a child.
758    ///
759    /// Loader uses this after a factory `provide`s so `get_isolated` can find
760    /// the new service under `Entry.isolate` while `get` still works on boot.
761    pub fn bind_isolate(&self, tid: TypeId, label: impl Into<Symbol>) {
762        self.isolate.write().insert(tid, label.into());
763    }
764
765    /// Record an intercept override on this context without forking a child.
766    /// APPENDS a layer: the effective value becomes the innermost (this one)
767    /// while outer layers stay inspectable through [`Self::intercept_chain`].
768    pub fn bind_intercept<T: Service>(&self, val: T) {
769        let tid = TypeId::of::<T>();
770        let any: Arc<dyn Any + Send + Sync> = Arc::new(val);
771        self.intercept.write().entry(tid).or_default().push(any);
772    }
773    // --- Name-keyed computed-property accessors ---------------------------
774
775    /// Register a computed property under `name` beside the TypeId service
776    /// store. Duplicate declarations (including alias collisions) are
777    /// rejected with [`CordisError::DuplicateProvider`]. The returned
778    /// [`EffectHandle`] removes the declaration (and any aliases) on
779    /// dispose.
780    ///
781    /// Accessor reads/writes BYPASS the `internal/get` / `internal/set`
782    /// intercept waterfalls entirely — resolving an accessor never consults
783    /// or re-enters a veto chain.
784    pub fn register_accessor(
785        self: &Arc<Self>,
786        name: &str,
787        accessor: Accessor,
788    ) -> Result<EffectHandle, CordisError> {
789        let mut accessors = self.accessors.write();
790        if accessors.contains_key(name) {
791            return Err(CordisError::DuplicateProvider {
792                name: name.to_string(),
793                owner: "accessor".to_string(),
794            });
795        }
796        let slot = std::sync::Arc::new(AccessorSlot {
797            getter: accessor.getter,
798            setter: accessor.setter,
799            names: parking_lot::Mutex::new(vec![name.to_string()]),
800        });
801        accessors.insert(name.to_string(), slot.clone());
802        Ok(EffectHandle {
803            ctx: Arc::downgrade(self),
804            slot: Arc::downgrade(&slot),
805        })
806    }
807
808    /// Bind `alias` as an alternate name resolving through the SAME
809    /// registration as `target` — same getter/setter, disposed together.
810    pub fn alias(self: &Arc<Self>, alias: &str, target: &str) -> Result<(), CordisError> {
811        let mut accessors = self.accessors.write();
812        let slot = accessors.get(target).cloned().ok_or_else(|| {
813            CordisError::ServiceNotFound(format!(
814                "cannot alias '{alias}': no property named '{target}'"
815            ))
816        })?;
817        if accessors.contains_key(alias) {
818            return Err(CordisError::DuplicateProvider {
819                name: alias.to_string(),
820                owner: "accessor".to_string(),
821            });
822        }
823        slot.names.lock().push(alias.to_string());
824        accessors.insert(alias.to_string(), slot);
825        Ok(())
826    }
827
828    /// Resolve `name` through its accessor (bypassing all interception
829    /// waterfalls). Undeclared names and write-only properties resolve
830    /// `None`; use [`Self::read_property_typed`] for downcast checking.
831    pub fn read_property(
832        &self,
833        name: &str,
834    ) -> Result<Option<Arc<dyn Any + Send + Sync>>, CordisError> {
835        let slot = self.accessors.read().get(name).cloned();
836        let Some(slot) = slot else {
837            return Ok(None);
838        };
839        match &slot.getter {
840            Some(getter) => getter(self),
841            None => Ok(None),
842        }
843    }
844
845    /// Typed accessor read: a value that fails to downcast to `T` is
846    /// [`CordisError::PropertyTypeMismatch`], never a silent `None`.
847    pub fn read_property_typed<T: Any + Send + Sync>(
848        &self,
849        name: &str,
850    ) -> Result<Option<Arc<T>>, CordisError> {
851        match self.read_property(name)? {
852            None => Ok(None),
853            Some(any) => any.downcast::<T>().map(Some).map_err(|_| {
854                CordisError::PropertyTypeMismatch {
855                    name: name.to_string(),
856                    expected: std::any::type_name::<T>().to_string(),
857                }
858            }),
859        }
860    }
861
862    /// Write `value` to `name` through its accessor. A fully undeclared
863    /// name is refused MissingService-style ("cannot set property"); a
864    /// declared-but-setter-less name is refused
865    /// [`CordisError::ReadOnlyProperty`]. Never consults the
866    /// `internal/set` waterfall.
867    pub fn write_property(
868        self: &Arc<Self>,
869        name: &str,
870        value: Arc<dyn Any + Send + Sync>,
871    ) -> Result<(), CordisError> {
872        let slot = self.accessors.read().get(name).cloned();
873        let Some(slot) = slot else {
874            return Err(CordisError::ServiceNotFound(format!(
875                "cannot set property '{name}': no accessor declared"
876            )));
877        };
878        let Some(setter) = &slot.setter else {
879            return Err(CordisError::ReadOnlyProperty(name.to_string()));
880        };
881        setter(self, value)
882    }
883
884    // --- Layered intercept chains -----------------------------------------
885
886    /// All intercept layers for `tid` visible from this frame, ordered
887    /// OUTERMOST..INNERMOST (ancestor frames first, this frame's appended
888    /// layers last). The innermost element is the effective value every
889    /// existing single-value getter returns.
890    pub fn intercept_chain(&self, tid: TypeId) -> Vec<Arc<dyn Any + Send + Sync>> {
891        let mut chain = match &self.parent {
892            // An isolate label is a realm boundary: layers beyond it do not
893            // leak in, mirroring the strict-read parent walk.
894            Some(parent)
895                if !self.isolate.read().contains_key(&tid)
896                    || parent.isolate_label(tid) == self.isolate_label(tid) =>
897            {
898                parent.intercept_chain(tid)
899            }
900            _ => Vec::new(),
901        };
902        if let Some(layers) = self.intercept.read().get(&tid) {
903            chain.extend(layers.iter().cloned());
904        }
905        chain
906    }
907
908    /// Structural equality for two intercepted chains, used by
909    /// restart-decision comparisons: same length and every layer pair the
910    /// SAME shared instance (`Arc::ptr_eq`). Erased values carry no
911    /// comparable contract, so identity is the only honest structural test;
912    /// freshly-built values therefore compare unequal by design.
913    pub fn chains_structurally_equal(
914        a: &[Arc<dyn Any + Send + Sync>],
915        b: &[Arc<dyn Any + Send + Sync>],
916    ) -> bool {
917        a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| Arc::ptr_eq(x, y))
918    }
919
920    /// Retrieve a service only if it was provided in a context whose isolate
921    /// namespace for `T` matches `label`. Walks the context chain but skips
922    /// any frame whose isolate label for `T` differs from the requested one.
923    pub fn get_isolated<T: Service>(&self, label: &str) -> Option<Arc<T>> {
924        let tid = TypeId::of::<T>();
925        let my_label = self.isolate.read().get(&tid).cloned();
926        match my_label.as_deref() {
927            Some(l) if l == label => {
928                // Matching namespace — check this context's store
929                if let Some(any) = self.store.read().get(&tid) {
930                    let active = self
931                        .owners
932                        .read()
933                        .get(&tid)
934                        .and_then(Weak::upgrade)
935                        .map(|fiber| matches!(fiber.state(), FiberState::Active { .. }))
936                        .unwrap_or(true);
937                    if active {
938                        if let Ok(arc) = any.clone().downcast::<T>() {
939                            if arc.check() {
940                                return Some(arc);
941                            }
942                        }
943                    }
944                }
945                // Continue up the chain
946                if let Some(parent) = &self.parent {
947                    return parent.get_isolated::<T>(label);
948                }
949                None
950            }
951            Some(_) => {
952                // Different namespace — do not look here or in parent
953                None
954            }
955            None => {
956                // No isolate entry for T in this frame — skip to parent
957                if let Some(parent) = &self.parent {
958                    return parent.get_isolated::<T>(label);
959                }
960                None
961            }
962        }
963    }
964
965    /// Create a child context where `get::<T>()` returns `val` as an override.
966    /// Alias for `intercept` — explicitly named for per-request model pinning.
967    pub fn with_intercept<T: Service>(self: &Arc<Self>, val: T) -> Arc<Self> {
968        self.intercept(val)
969    }
970
971    /// Wait until `T` is provided on this context (or a parent). Returns the service.
972    ///
973    /// If [`ReflectService`] is on the context, wait on its `TypeId` notifier
974    /// (`ensure_notifier` + `changed`) so `provide` → `notify` unblocks without
975    /// polling. If the sender is dropped, or ReflectService is absent, fall
976    /// through to a 5ms poll loop so tests without Reflect still complete.
977    pub async fn inject<T: Service>(self: &Arc<Self>) -> Arc<T> {
978        if let Some(value) = self.get::<T>() {
979            return value;
980        }
981        if let Some(reflect) = self.get::<ReflectService>() {
982            let mut rx = reflect.ensure_notifier(TypeId::of::<T>());
983            loop {
984                if let Some(value) = self.get::<T>() {
985                    return value;
986                }
987                if rx.changed().await.is_err() {
988                    break;
989                }
990            }
991        }
992        loop {
993            if let Some(value) = self.get::<T>() {
994                return value;
995            }
996            tokio::time::sleep(std::time::Duration::from_millis(5)).await;
997        }
998    }
999
1000    pub fn provide_arc<T: Service>(self: &Arc<Self>, svc: Arc<T>) -> Arc<T> {
1001        let owner = self.active_provider_fiber();
1002        self.provide_impl(svc, owner.as_ref(), None)
1003    }
1004
1005    pub fn fiber(&self) -> Arc<Fiber> {
1006        self.fiber.clone()
1007    }
1008
1009    // Snapshot for temporal test: capture store length + versions
1010    pub fn snapshot_len(&self) -> usize {
1011        self.store.read().len()
1012    }
1013
1014    pub async fn plugin<S: Service>(self: &Arc<Self>, svc: S) -> Result<FiberId, CordisError> {
1015        let tid = TypeId::of::<S>();
1016        if self.store.read().contains_key(&tid) {
1017            return Err(Self::duplicate_provider_error(tid));
1018        }
1019        let fiber = self.fiber.clone();
1020        fiber.set_state(FiberState::Loading);
1021        let disposable = match svc.init(self).await {
1022            Ok(d) => d,
1023            Err(e) => {
1024                fiber.set_state(FiberState::Failed {
1025                    error: Some(e.to_string()),
1026                });
1027                return Err(e);
1028            }
1029        };
1030        let svc_arc = self.provide(svc);
1031        let fid = NEXT_FIBER_ID.fetch_add(1, Ordering::SeqCst) as u64;
1032        if let Some(d) = disposable {
1033            let undo: Box<dyn FnOnce() + Send> = Box::new(move || {
1034                d.dispose();
1035            });
1036            fiber.push_undo(undo);
1037        }
1038        if svc_arc.check() {
1039            let epoch = fiber.compute_epoch(self);
1040            fiber.set_epoch(epoch.clone());
1041            fiber.set_state(FiberState::Active { epoch });
1042        } else {
1043            fiber.set_state(FiberState::Inactive { error: None });
1044            if let Some(reflect) = self.get::<ReflectService>() {
1045                reflect.notify(tid);
1046            }
1047        }
1048        if let Some(reflect) = self.get::<ReflectService>() {
1049            let _ = reflect.ensure_notifier(tid);
1050            reflect.set_context(self);
1051        }
1052        Ok(fid)
1053    }
1054
1055    pub async fn plugin_with<P: Plugin>(
1056        self: &Arc<Self>,
1057        plugin: P,
1058        config: P::Config,
1059    ) -> Result<FiberId, CordisError> {
1060        if let Some(registry) = self.get::<RegistryService>() {
1061            return registry.plugin(self, plugin, config);
1062        }
1063        let tid = TypeId::of::<P::Provides>();
1064        if self.store.read().contains_key(&tid) {
1065            return Err(Self::duplicate_provider_error(tid));
1066        }
1067        self.fiber.set_state(FiberState::Loading);
1068        let provides = match plugin.apply(self, config) {
1069            Ok(p) => p,
1070            Err(e) => {
1071                self.fiber.set_state(FiberState::Failed {
1072                    error: Some(e.to_string()),
1073                });
1074                return Err(e);
1075            }
1076        };
1077        self.provide_arc(provides);
1078        let epoch = self.fiber.compute_epoch(self);
1079        self.fiber.set_epoch(epoch.clone());
1080        self.fiber.set_state(FiberState::Active { epoch });
1081        let fid = NEXT_FIBER_ID.fetch_add(1, Ordering::SeqCst) as u64;
1082        Ok(fid)
1083    }
1084}
1085
1086#[cfg(test)]
1087mod relaxed_tests {
1088    use super::*;
1089    use crate::fiber::FiberState;
1090
1091    /// `get_relaxed` reads a locally-owned value while its owner fiber sits
1092    /// mid-transition (`Loading`/`Reloading`/`Unloading`/`Pending`), where
1093    /// strict [`Context::get`] still refuses; terminal/disposed owners stay
1094    /// refused even in relaxed mode.
1095    #[tokio::test]
1096    async fn relaxed_read_succeeds_while_provider_transitioning() {
1097        #[derive(Debug)]
1098        struct TransitionProbe(u32);
1099        impl Service for TransitionProbe {}
1100
1101        let ctx = Context::new_root();
1102        let fiber = Arc::new(Fiber::new());
1103        fiber.set_reload_context(&ctx);
1104        fiber.set_id(96_001);
1105
1106        // Provide ON the registration fiber so the owner link exists — the
1107        // provide_on_fiber path used by RegistryService.
1108        let svc = Arc::new(TransitionProbe(7));
1109        ctx.provide_on_fiber(svc, &fiber);
1110
1111        // Strict get refuses non-Active owners...
1112        assert!(ctx.get::<TransitionProbe>().is_none());
1113
1114        for state in [
1115            FiberState::Loading,
1116            FiberState::Reloading,
1117            FiberState::Unloading { error: None },
1118            FiberState::Pending,
1119        ] {
1120            fiber.set_state(state.clone());
1121            let relaxed = ctx.get_relaxed::<TransitionProbe>();
1122            assert!(
1123                relaxed.is_some(),
1124                "relaxed read must succeed in {state:?}"
1125            );
1126            assert_eq!(
1127                relaxed.as_ref().map(|s| s.0),
1128                Some(7),
1129                "the transitioning value itself is served"
1130            );
1131        }
1132
1133        // Terminal Failed stays refused even in relaxed mode.
1134        fiber.set_state(FiberState::Failed {
1135            error: Some("boom".into()),
1136        });
1137        assert!(
1138            ctx.get_relaxed::<TransitionProbe>().is_none(),
1139            "Failed owner must stay invisible to relaxed reads"
1140        );
1141
1142        // Disposed owners stay refused too (dispose rests Inactive + flag).
1143        fiber.set_state(FiberState::Inactive { error: None });
1144        let _ = fiber.dispose().await;
1145        assert!(
1146            ctx.get_relaxed::<TransitionProbe>().is_none(),
1147            "disposed owner must stay invisible to relaxed reads"
1148        );
1149    }
1150}
1151
1152#[cfg(test)]
1153mod accessor_tests {
1154    use super::*;
1155    use parking_lot::Mutex as StdMutex;
1156
1157    #[derive(Debug, PartialEq)]
1158    struct PropValue(pub u64);
1159
1160    fn read_cell_getter(
1161        cell: Arc<StdMutex<u64>>,
1162    ) -> impl Fn(&Context) -> Result<Option<Arc<dyn Any + Send + Sync>>, CordisError>
1163           + Send
1164           + Sync
1165           + 'static {
1166        move |_| {
1167            Ok(Some(Arc::new(PropValue(*cell.lock()))
1168                as Arc<dyn Any + Send + Sync>))
1169        }
1170    }
1171
1172    #[test]
1173    fn accessor_read_write_roundtrip() {
1174        let ctx = Context::new_root();
1175        let cell = Arc::new(StdMutex::new(1u64));
1176        let write_cell = cell.clone();
1177        let _handle = ctx
1178            .register_accessor(
1179                "quota",
1180                Accessor::read_write(
1181                    read_cell_getter(cell.clone()),
1182                    move |_ctx, value: Arc<dyn Any + Send + Sync>| {
1183                        let v = value
1184                            .downcast::<PropValue>()
1185                            .map_err(|_| CordisError::Internal("bad property type".into()))?;
1186                        *write_cell.lock() = v.0;
1187                        Ok(())
1188                    },
1189                ),
1190            )
1191            .unwrap();
1192
1193        let got = ctx.read_property_typed::<PropValue>("quota").unwrap().unwrap();
1194        assert_eq!(*got, PropValue(1));
1195        ctx.write_property("quota", Arc::new(PropValue(42))).unwrap();
1196        let got = ctx.read_property_typed::<PropValue>("quota").unwrap().unwrap();
1197        assert_eq!(*got, PropValue(42));
1198        assert_eq!(*cell.lock(), 42);
1199
1200        // A wrong typed read is a PropertyTypeMismatch, never a silent None.
1201        match ctx.read_property_typed::<String>("quota") {
1202            Err(CordisError::PropertyTypeMismatch { name, .. }) => assert_eq!(name, "quota"),
1203            other => panic!("expected PropertyTypeMismatch, got {other:?}"),
1204        }
1205    }
1206
1207    #[test]
1208    fn duplicate_accessor_declaration_rejected() {
1209        let ctx = Context::new_root();
1210        ctx.register_accessor("dup", Accessor::read_only(|_| Ok(None)))
1211            .expect("first declaration wins");
1212        match ctx.register_accessor("dup", Accessor::read_only(|_| Ok(None))) {
1213            Err(CordisError::DuplicateProvider { name, owner }) => {
1214                assert_eq!(name, "dup");
1215                assert_eq!(owner, "accessor");
1216            }
1217            Err(other) => panic!("expected DuplicateProvider, got {other:?}"),
1218            Ok(_) => panic!("duplicate declaration must be rejected"),
1219        }
1220    }
1221
1222    #[test]
1223    fn readonly_property_rejects_set() {
1224        let ctx = Context::new_root();
1225        let _handle = ctx
1226            .register_accessor(
1227                "ro",
1228                Accessor::read_only(read_cell_getter(Arc::new(StdMutex::new(7u64)))),
1229            )
1230            .unwrap();
1231        let err = ctx.write_property("ro", Arc::new(PropValue(9))).unwrap_err();
1232        assert!(matches!(err, CordisError::ReadOnlyProperty(ref n) if n == "ro"));
1233        // The stored resolution is untouched.
1234        assert_eq!(
1235            *ctx.read_property_typed::<PropValue>("ro").unwrap().unwrap(),
1236            PropValue(7)
1237        );
1238    }
1239
1240    #[test]
1241    fn dispose_accessor_resolves_none() {
1242        let ctx = Context::new_root();
1243        let handle = ctx
1244            .register_accessor("gone", Accessor::read_only(|_| Ok(None)))
1245            .unwrap();
1246        assert!(ctx.read_property("gone").unwrap().is_none());
1247        assert!(handle.dispose(), "live handle reports removal");
1248        // Still resolves none afterwards, but writes now hit the undeclared path.
1249        assert!(ctx.read_property("gone").unwrap().is_none());
1250        let err = ctx.write_property("gone", Arc::new(PropValue(1))).unwrap_err();
1251        assert!(
1252            matches!(err, CordisError::ServiceNotFound(ref m) if m.contains("cannot set property")),
1253            "unexpected error: {err}"
1254        );
1255        // A re-registered declaration is disposable again.
1256        let handle2 = ctx
1257            .register_accessor("gone2", Accessor::read_only(|_| Ok(None)))
1258            .unwrap();
1259        assert!(handle2.dispose());
1260        assert!(ctx.read_property("gone2").unwrap().is_none());
1261    }
1262
1263    #[test]
1264    fn alias_resolves_same_value() {
1265        let ctx = Context::new_root();
1266        let cell = Arc::new(StdMutex::new(5u64));
1267        let write_cell = cell.clone();
1268        let handle = ctx
1269            .register_accessor(
1270                "primary",
1271                Accessor::read_write(
1272                    read_cell_getter(cell),
1273                    move |_ctx, value: Arc<dyn Any + Send + Sync>| {
1274                        *write_cell.lock() =
1275                            value.downcast::<PropValue>().unwrap().0;
1276                        Ok(())
1277                    },
1278                ),
1279            )
1280            .unwrap();
1281        ctx.alias("nick", "primary").expect("alias binds");
1282
1283        // Same getter through the alias.
1284        assert_eq!(
1285            *ctx.read_property_typed::<PropValue>("nick").unwrap().unwrap(),
1286            PropValue(5)
1287        );
1288        // Same setter through the alias.
1289        ctx.write_property("nick", Arc::new(PropValue(6))).unwrap();
1290        assert_eq!(
1291            *ctx.read_property_typed::<PropValue>("primary").unwrap().unwrap(),
1292            PropValue(6)
1293        );
1294
1295        // Collisions and unknown targets are refused.
1296        assert!(matches!(
1297            ctx.alias("nick", "primary"),
1298            Err(CordisError::DuplicateProvider { .. })
1299        ));
1300        assert!(matches!(
1301            ctx.alias("x", "missing"),
1302            Err(CordisError::ServiceNotFound(_))
1303        ));
1304
1305        // Disposing the original registration removes BOTH names.
1306        assert!(handle.dispose());
1307        assert!(ctx.read_property("primary").unwrap().is_none());
1308        assert!(ctx.read_property("nick").unwrap().is_none());
1309    }
1310
1311    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1312    async fn accessor_bypasses_intercept_waterfalls() {
1313        struct Marker;
1314        impl crate::Service for Marker {}
1315
1316        let ctx = Context::new_root();
1317        let events = Arc::new(crate::EventsService::new());
1318        ctx.provide_arc(events.clone());
1319
1320        // Refuse every strict service read and veto every service write.
1321        let _get_gate = events
1322            .on(crate::events::INTERNAL_GET_EVENT.into(), |_p| async move {
1323                Ok(serde_json::json!({ "refuse": true }))
1324            });
1325        let _set_gate = events
1326            .on(crate::events::INTERNAL_SET_EVENT.into(), |_p| async move {
1327                Ok(serde_json::json!("vetoed"))
1328            });
1329
1330        // Sanity: with the gates up, the strict paths ARE intercepted.
1331        ctx.provide(Marker);
1332        assert!(
1333            ctx.get::<Marker>().is_none(),
1334            "internal/get waterfall must refuse strict reads in this test"
1335        );
1336
1337        // The accessor path ignores both waterfalls entirely.
1338        let cell = Arc::new(StdMutex::new(3u64));
1339        let write_cell = cell.clone();
1340        let _handle = ctx
1341            .register_accessor(
1342                "open",
1343                Accessor::read_write(
1344                    read_cell_getter(cell),
1345                    move |_c, value: Arc<dyn Any + Send + Sync>| {
1346                        *write_cell.lock() =
1347                            value.downcast::<PropValue>().unwrap().0;
1348                        Ok(())
1349                    },
1350                ),
1351            )
1352            .unwrap();
1353        assert_eq!(
1354            *ctx.read_property_typed::<PropValue>("open").unwrap().unwrap(),
1355            PropValue(3),
1356            "accessor read bypasses internal/get"
1357        );
1358        ctx.write_property("open", Arc::new(PropValue(4))).unwrap();
1359        assert_eq!(
1360            *ctx.read_property_typed::<PropValue>("open").unwrap().unwrap(),
1361            PropValue(4),
1362            "accessor write bypasses internal/set"
1363        );
1364    }
1365}
1366
1367#[cfg(test)]
1368mod intercept_chain_tests {
1369    use super::*;
1370
1371    #[derive(Debug)]
1372    struct LayerSvc(pub u64);
1373    impl crate::Service for LayerSvc {}
1374
1375    #[tokio::test]
1376    async fn chained_layers_append_innermost_effective() {
1377        let ctx = Context::new_root();
1378        ctx.bind_intercept(LayerSvc(1));
1379        assert_eq!(ctx.get::<LayerSvc>().unwrap().0, 1);
1380        ctx.bind_intercept(LayerSvc(2));
1381        assert_eq!(ctx.get::<LayerSvc>().unwrap().0, 2, "innermost layer wins");
1382        let chain = ctx.intercept_chain(TypeId::of::<LayerSvc>());
1383        assert_eq!(chain.len(), 2);
1384        assert_eq!(chain[0].clone().downcast::<LayerSvc>().unwrap().0, 1);
1385        assert_eq!(chain[1].clone().downcast::<LayerSvc>().unwrap().0, 2);
1386    }
1387
1388    #[tokio::test]
1389    async fn intercept_chain_returns_all_layers_in_order() {
1390        let root = Context::new_root();
1391        let mid = root.intercept(LayerSvc(10));
1392        let leaf = mid.intercept(LayerSvc(11));
1393        leaf.bind_intercept(LayerSvc(12));
1394
1395        let chain = leaf.intercept_chain(TypeId::of::<LayerSvc>());
1396        assert_eq!(chain.len(), 3);
1397        let vals: Vec<u64> = chain
1398            .iter()
1399            .map(|a| a.clone().downcast::<LayerSvc>().unwrap().0)
1400            .collect();
1401        assert_eq!(vals, vec![10, 11, 12], "outermost..innermost order");
1402        assert_eq!(leaf.get::<LayerSvc>().unwrap().0, 12);
1403
1404        // Structural equality: the same chain matches its own snapshot but
1405        // not an identically-built chain of fresh values.
1406        assert!(Context::chains_structurally_equal(
1407            &chain,
1408            &leaf.intercept_chain(TypeId::of::<LayerSvc>())
1409        ));
1410        let fresh_root = Context::new_root();
1411        let fresh_mid = fresh_root.intercept(LayerSvc(10));
1412        let fresh_leaf = fresh_mid.intercept(LayerSvc(11));
1413        fresh_leaf.bind_intercept(LayerSvc(12));
1414        assert!(!Context::chains_structurally_equal(
1415            &chain,
1416            &fresh_leaf.intercept_chain(TypeId::of::<LayerSvc>())
1417        ));
1418    }
1419
1420    #[tokio::test]
1421    async fn inject_appends_layer() {
1422        let ctx = Context::new_root();
1423        let tid = TypeId::of::<LayerSvc>();
1424
1425        // Untyped plugin-style injection APPENDS instead of replacing.
1426        ctx.bind_intercept_untyped(tid, Arc::new(LayerSvc(20)) as Arc<dyn Any + Send + Sync>);
1427        ctx.bind_intercept_untyped(tid, Arc::new(LayerSvc(21)) as Arc<dyn Any + Send + Sync>);
1428        assert_eq!(ctx.intercept_chain(tid).len(), 2);
1429        assert_eq!(ctx.get::<LayerSvc>().unwrap().0, 21);
1430
1431        // Popping the innermost layer restores the outer one as effective.
1432        ctx.remove_intercept_untyped(tid);
1433        assert_eq!(ctx.intercept_chain(tid).len(), 1);
1434        assert_eq!(ctx.get::<LayerSvc>().unwrap().0, 20);
1435        ctx.remove_intercept_untyped(tid);
1436        assert!(ctx.intercept_chain(tid).is_empty());
1437        assert!(ctx.get::<LayerSvc>().is_none());
1438    }
1439}