Skip to main content

cordis/
reflect.rs

1//! Scoped service storage and explicit reflection APIs.
2
3use crate::context::{Context, Isolation, RootInner};
4use crate::effect::{AsyncDisposer, EffectHandle};
5use crate::fiber::{Fiber, FiberInner, FiberState};
6use crate::registry::Inject;
7use crate::utils::lock;
8use crate::{CordisError, ErrorCode, Result, Value};
9use std::collections::HashMap;
10use std::fmt::{self, Debug, Formatter};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, Mutex, Weak};
13
14/// Dynamic getter used by computed context properties.
15pub type AccessorGet = Arc<dyn Fn(&Context) -> Result<Option<Value>> + Send + Sync + 'static>;
16/// Dynamic setter used by computed context properties.
17pub type AccessorSet = Arc<dyn Fn(&Context, Value) -> Result<()> + Send + Sync + 'static>;
18
19/// Explicit replacement for a JavaScript proxy-backed computed property.
20#[derive(Clone)]
21pub struct Accessor {
22    /// Getter callback.
23    pub get: AccessorGet,
24    /// Optional setter callback.
25    pub set: Option<AccessorSet>,
26}
27
28impl Accessor {
29    /// Construct a read-only accessor.
30    pub fn read_only<F>(get: F) -> Self
31    where
32        F: Fn(&Context) -> Result<Option<Value>> + Send + Sync + 'static,
33    {
34        Self {
35            get: Arc::new(get),
36            set: None,
37        }
38    }
39
40    /// Construct a read/write accessor.
41    pub fn read_write<G, S>(get: G, set: S) -> Self
42    where
43        G: Fn(&Context) -> Result<Option<Value>> + Send + Sync + 'static,
44        S: Fn(&Context, Value) -> Result<()> + Send + Sync + 'static,
45    {
46        Self {
47            get: Arc::new(get),
48            set: Some(Arc::new(set)),
49        }
50    }
51}
52
53impl Debug for Accessor {
54    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
55        f.debug_struct("Accessor")
56            .field("writable", &self.set.is_some())
57            .finish_non_exhaustive()
58    }
59}
60
61/// Reflected property declaration.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum Property {
64    /// Scoped service implementation.
65    Service,
66    /// Dynamic getter/setter.
67    Accessor,
68}
69
70struct AccessorRecord {
71    owner_uid: u64,
72    accessor: Accessor,
73}
74
75type AvailabilityCheck = Arc<dyn Fn(&Value) -> bool + Send + Sync + 'static>;
76
77pub(crate) struct ServiceImpl {
78    name: String,
79    value: Value,
80    fiber: Weak<FiberInner>,
81    provider_uid: u64,
82    generation: u64,
83    check: Option<AvailabilityCheck>,
84}
85
86#[derive(Default)]
87struct ReflectState {
88    default_scopes: HashMap<String, Isolation>,
89    implementations: HashMap<Isolation, ServiceImpl>,
90    properties: HashMap<String, Property>,
91    accessors: HashMap<String, AccessorRecord>,
92}
93
94pub(crate) struct ReflectRoot {
95    state: Mutex<ReflectState>,
96    next_generation: AtomicU64,
97}
98
99impl ReflectRoot {
100    pub(crate) fn new() -> Self {
101        Self {
102            state: Mutex::new(ReflectState::default()),
103            next_generation: AtomicU64::new(0),
104        }
105    }
106
107    fn next_generation(&self) -> u64 {
108        self.next_generation.fetch_add(1, Ordering::Relaxed) + 1
109    }
110
111    fn scope_for(&self, ctx: &Context, name: &str) -> Option<Isolation> {
112        ctx.scope_override(name)
113            .or_else(|| lock(&self.state).default_scopes.get(name).copied())
114    }
115
116    fn ensure_scope(&self, ctx: &Context, name: &str) -> Isolation {
117        if let Some(scope) = ctx.scope_override(name) {
118            return scope;
119        }
120        let mut state = lock(&self.state);
121        if let Some(scope) = state.default_scopes.get(name) {
122            return *scope;
123        }
124        let scope = ctx.root.scope();
125        state.default_scopes.insert(name.to_owned(), scope);
126        scope
127    }
128
129    fn value(&self, ctx: &Context, name: &str, strict: bool) -> Result<Option<Value>> {
130        // Scope overrides live in the lock-free context meta, so one guard
131        // covers the whole lookup.
132        let scope_override = ctx.scope_override(name);
133        let state = lock(&self.state);
134        if state.properties.get(name) == Some(&Property::Accessor) {
135            let accessor = state
136                .accessors
137                .get(name)
138                .map(|record| record.accessor.clone());
139            drop(state);
140            return match accessor {
141                Some(accessor) => (accessor.get)(ctx),
142                None => Ok(None),
143            };
144        }
145
146        let Some(scope) = scope_override.or_else(|| state.default_scopes.get(name).copied()) else {
147            return Ok(None);
148        };
149        let Some(implementation) = state.implementations.get(&scope) else {
150            return Ok(None);
151        };
152        if implementation.name != name {
153            return Ok(None);
154        }
155        let value = implementation.value.clone();
156        let provider = implementation.fiber.upgrade();
157        let provider_uid = implementation.provider_uid;
158        drop(state);
159
160        let Some(provider) = provider else {
161            return Ok(None);
162        };
163        if strict {
164            let caller_uid = ctx.fiber().ok().and_then(|fiber| fiber.uid());
165            if caller_uid != Some(provider_uid)
166                && Fiber::from_inner(provider).state() != FiberState::Active
167            {
168                return Ok(None);
169            }
170        }
171        Ok(Some(value))
172    }
173
174    fn implementation_epoch(&self, ctx: &Context, name: &str) -> Option<u64> {
175        let scope_override = ctx.scope_override(name);
176        let (generation, provider, check, value) = {
177            let state = lock(&self.state);
178            let scope = scope_override.or_else(|| state.default_scopes.get(name).copied())?;
179            let implementation = state.implementations.get(&scope)?;
180            if implementation.name != name {
181                return None;
182            }
183            (
184                implementation.generation,
185                implementation.fiber.upgrade(),
186                implementation.check.clone(),
187                // The value only serves the availability check; cloning it
188                // when no check is registered is a wasted Arc round-trip per
189                // dependency per fiber reconcile.
190                implementation
191                    .check
192                    .as_ref()
193                    .map(|_| implementation.value.clone()),
194            )
195        };
196        let provider = provider?;
197        if Fiber::from_inner(provider).state() != FiberState::Active {
198            return None;
199        }
200        if let (Some(check), Some(value)) = (check, value) {
201            if !check(&value) {
202                return None;
203            }
204        }
205        Some(generation)
206    }
207
208    fn services_owned_by(&self, uid: u64) -> Vec<(String, Isolation)> {
209        lock(&self.state)
210            .implementations
211            .iter()
212            .filter(|(_, implementation)| implementation.provider_uid == uid)
213            .map(|(scope, implementation)| (implementation.name.clone(), *scope))
214            .collect()
215    }
216}
217
218/// Public service implementation diagnostics.
219#[derive(Debug, Clone)]
220pub struct ServiceInfo {
221    /// Service name.
222    pub name: String,
223    /// Scope label.
224    pub isolation: Isolation,
225    /// Provider fiber id.
226    pub provider_uid: u64,
227    /// Provider display name.
228    pub provider_name: String,
229    /// Concrete Rust type name.
230    pub type_name: &'static str,
231    /// Generation used in dependency epochs.
232    pub generation: u64,
233}
234
235/// Reflection and service-resolution API bound to a context.
236#[derive(Clone, Debug)]
237pub struct ReflectService {
238    ctx: Context,
239}
240
241impl ReflectService {
242    pub(crate) fn new(ctx: Context) -> Self {
243        Self { ctx }
244    }
245
246    /// Read and downcast a service.
247    pub fn get<T>(&self, name: &str, strict: bool) -> Result<Option<Arc<T>>>
248    where
249        T: Send + Sync + 'static,
250    {
251        self.get_value(name, strict)?
252            .map(|value| value.downcast())
253            .transpose()
254    }
255
256    /// Read a type-erased service or accessor result.
257    pub fn get_value(&self, name: &str, strict: bool) -> Result<Option<Value>> {
258        self.ctx.root.reflect.value(&self.ctx, name, strict)
259    }
260
261    /// Require and downcast an active service.
262    pub fn require<T>(&self, name: &str) -> Result<Arc<T>>
263    where
264        T: Send + Sync + 'static,
265    {
266        let value = self.get_value(name, true)?.ok_or_else(|| {
267            CordisError::with_message(
268                ErrorCode::MissingService,
269                format!("required service \"{name}\" is unavailable"),
270            )
271        })?;
272        value.downcast()
273    }
274
275    /// Register a type-erased service and optional availability predicate.
276    pub fn provide_value(
277        &self,
278        name: String,
279        value: Value,
280        check: Option<AvailabilityCheck>,
281    ) -> Result<EffectHandle> {
282        let fiber = self.ctx.fiber()?;
283        fiber.assert_active()?;
284        let uid = fiber
285            .uid()
286            .ok_or_else(|| CordisError::new(ErrorCode::InactiveEffect))?;
287        let scope = self.ctx.root.reflect.ensure_scope(&self.ctx, &name);
288        let generation = self.ctx.root.reflect.next_generation();
289
290        {
291            let mut state = lock(&self.ctx.root.reflect.state);
292            if let Some(property) = state.properties.get(&name) {
293                if *property != Property::Service {
294                    return Err(CordisError::with_message(
295                        ErrorCode::PropertyConflict,
296                        format!("property \"{name}\" is already declared as {property:?}"),
297                    ));
298                }
299            }
300            if let Some(existing) = state.implementations.get(&scope) {
301                let provider_name = existing
302                    .fiber
303                    .upgrade()
304                    .map(Fiber::from_inner)
305                    .map(|fiber| fiber.name())
306                    .unwrap_or_else(|| "disposed".to_owned());
307                return Err(CordisError::with_message(
308                    ErrorCode::DuplicateService,
309                    format!("service \"{name}\" has been registered at <{provider_name}>"),
310                ));
311            }
312            state.properties.insert(name.clone(), Property::Service);
313            state.implementations.insert(
314                scope,
315                ServiceImpl {
316                    name: name.clone(),
317                    value,
318                    fiber: Arc::downgrade(&fiber.inner),
319                    provider_uid: uid,
320                    generation,
321                    check,
322                },
323            );
324        }
325
326        let root = Arc::downgrade(&self.ctx.root);
327        let effect_name = name.clone();
328        let effect = fiber.register_effect(
329            format!("ctx.provide({name:?})"),
330            AsyncDisposer::from_sync(move || {
331                let Some(root) = root.upgrade() else {
332                    return Ok(());
333                };
334                let removed = {
335                    let mut state = lock(&root.reflect.state);
336                    match state.implementations.get(&scope) {
337                        Some(current) if current.generation == generation => {
338                            state.implementations.remove(&scope);
339                            true
340                        }
341                        _ => false,
342                    }
343                };
344                if removed {
345                    root.notify_service(&effect_name, scope);
346                }
347                Ok(())
348            }),
349        );
350
351        match effect {
352            Ok(effect) => {
353                self.ctx.root.notify_service(&name, scope);
354                Ok(effect)
355            }
356            Err(error) => {
357                lock(&self.ctx.root.reflect.state)
358                    .implementations
359                    .remove(&scope);
360                Err(error)
361            }
362        }
363    }
364
365    /// Register a service with an availability predicate.
366    pub fn provide_checked<T, F>(
367        &self,
368        name: impl Into<String>,
369        value: T,
370        check: F,
371    ) -> Result<EffectHandle>
372    where
373        T: Send + Sync + 'static,
374        F: Fn(&T) -> bool + Send + Sync + 'static,
375    {
376        let value = Value::new(value);
377        let check = Arc::new(move |value: &Value| {
378            value
379                .as_any()
380                .downcast_ref::<T>()
381                .map(&check)
382                .unwrap_or(false)
383        });
384        self.provide_value(name.into(), value, Some(check))
385    }
386
387    /// Replace a service or computed property value.
388    pub fn set_value(&self, name: &str, value: Value) -> Result<()> {
389        let scope_override = self.ctx.scope_override(name);
390        let mut state = lock(&self.ctx.root.reflect.state);
391        if state.properties.get(name) == Some(&Property::Accessor) {
392            let setter = state
393                .accessors
394                .get(name)
395                .and_then(|record| record.accessor.set.clone());
396            drop(state);
397            let setter = setter.ok_or_else(|| {
398                CordisError::with_message(
399                    ErrorCode::AccessDenied,
400                    format!("property \"{name}\" is read-only"),
401                )
402            })?;
403            return setter(&self.ctx, value);
404        }
405
406        let scope = scope_override
407            .or_else(|| state.default_scopes.get(name).copied())
408            .ok_or_else(|| {
409                CordisError::with_message(
410                    ErrorCode::MissingService,
411                    format!("cannot set property \"{name}\" without provide"),
412                )
413            })?;
414        let uid = self.ctx.fiber()?.uid();
415        let implementation = state.implementations.get_mut(&scope).ok_or_else(|| {
416            CordisError::with_message(
417                ErrorCode::MissingService,
418                format!("cannot set property \"{name}\" without provide"),
419            )
420        })?;
421        if uid != Some(implementation.provider_uid) {
422            return Err(CordisError::with_message(
423                ErrorCode::AccessDenied,
424                format!("cannot set property \"{name}\" in multiple fibers"),
425            ));
426        }
427        implementation.value = value;
428        Ok(())
429    }
430
431    /// Define a dynamic computed context property.
432    pub fn accessor(&self, name: String, accessor: Accessor) -> Result<EffectHandle> {
433        let fiber = self.ctx.fiber()?;
434        let uid = fiber
435            .uid()
436            .ok_or_else(|| CordisError::new(ErrorCode::InactiveEffect))?;
437        {
438            let mut state = lock(&self.ctx.root.reflect.state);
439            if let Some(property) = state.properties.get(&name) {
440                return Err(CordisError::with_message(
441                    ErrorCode::PropertyConflict,
442                    format!("property \"{name}\" is already declared as {property:?}"),
443                ));
444            }
445            state.properties.insert(name.clone(), Property::Accessor);
446            state.accessors.insert(
447                name.clone(),
448                AccessorRecord {
449                    owner_uid: uid,
450                    accessor,
451                },
452            );
453        }
454        let root = Arc::downgrade(&self.ctx.root);
455        let effect_name = name.clone();
456        let effect = fiber.register_effect(
457            format!("ctx.accessor({name:?})"),
458            AsyncDisposer::from_sync(move || {
459                if let Some(root) = root.upgrade() {
460                    let mut state = lock(&root.reflect.state);
461                    let owned = state
462                        .accessors
463                        .get(&effect_name)
464                        .map(|record| record.owner_uid == uid)
465                        .unwrap_or(false);
466                    if owned {
467                        state.accessors.remove(&effect_name);
468                        state.properties.remove(&effect_name);
469                    }
470                }
471                Ok(())
472            }),
473        );
474        if effect.is_err() {
475            let mut state = lock(&self.ctx.root.reflect.state);
476            state.accessors.remove(&name);
477            state.properties.remove(&name);
478        }
479        effect
480    }
481
482    /// Alias one reflected service name to another. This is the explicit Rust
483    /// counterpart of the original dynamic `mixin()` forwarding.
484    pub fn alias(
485        &self,
486        alias: impl Into<String>,
487        target: impl Into<String>,
488    ) -> Result<EffectHandle> {
489        let alias = alias.into();
490        let target = target.into();
491        let getter_target = target.clone();
492        let setter_target = target;
493        self.accessor(
494            alias,
495            Accessor::read_write(
496                move |ctx| ctx.reflect().get_value(&getter_target, true),
497                move |ctx, value| ctx.reflect().set_value(&setter_target, value),
498            ),
499        )
500    }
501
502    /// Re-evaluate fibers that inject any of the named services.
503    ///
504    /// This is useful after an availability predicate registered with
505    /// [`provide_checked`](Self::provide_checked) changes without replacing
506    /// the service value. The returned fibers were in a matching isolation
507    /// scope and were asked to reconcile.
508    pub fn notify<I, S>(&self, names: I) -> Vec<Fiber>
509    where
510        I: IntoIterator<Item = S>,
511        S: AsRef<str>,
512    {
513        let scoped_names = names
514            .into_iter()
515            .filter_map(|name| {
516                let name = name.as_ref().to_owned();
517                self.ctx
518                    .root
519                    .reflect
520                    .scope_for(&self.ctx, &name)
521                    .map(|scope| (name, scope))
522            })
523            .collect::<Vec<_>>();
524        let mut affected: Vec<Fiber> = Vec::new();
525        for (name, scope) in scoped_names {
526            for fiber in self.ctx.root.notify_service(&name, scope) {
527                if !affected
528                    .iter()
529                    .any(|seen| Arc::ptr_eq(&seen.inner, &fiber.inner))
530                {
531                    affected.push(fiber);
532                }
533            }
534        }
535        affected
536    }
537
538    /// List reflected property declarations.
539    pub fn properties(&self) -> HashMap<String, Property> {
540        lock(&self.ctx.root.reflect.state).properties.clone()
541    }
542
543    /// List concrete service implementations in all isolation scopes.
544    pub fn services(&self) -> Vec<ServiceInfo> {
545        lock(&self.ctx.root.reflect.state)
546            .implementations
547            .iter()
548            .map(|(scope, implementation)| ServiceInfo {
549                name: implementation.name.clone(),
550                isolation: *scope,
551                provider_uid: implementation.provider_uid,
552                provider_name: implementation
553                    .fiber
554                    .upgrade()
555                    .map(Fiber::from_inner)
556                    .map(|fiber| fiber.name())
557                    .unwrap_or_else(|| "disposed".to_owned()),
558                type_name: implementation.value.type_name(),
559                generation: implementation.generation,
560            })
561            .collect()
562    }
563}
564
565impl RootInner {
566    pub(crate) fn dependency_epoch(&self, ctx: &Context, inject: &Inject) -> Option<Vec<u64>> {
567        // Generations aligned with inject order. Names are never read, only
568        // compared for equality, so cloning them per refresh is pure waste.
569        let mut epoch = Vec::with_capacity(inject.len());
570        for dependency in inject.iter() {
571            epoch.push(self.reflect.implementation_epoch(ctx, &dependency.name)?);
572        }
573        Some(epoch)
574    }
575
576    pub(crate) fn notify_service(&self, name: &str, scope: Isolation) -> Vec<Fiber> {
577        let mut refreshed = Vec::new();
578        for fiber in self.registry.fibers_injecting(name) {
579            let same_scope = fiber
580                .scope_override(name)
581                .or_else(|| lock(&self.reflect.state).default_scopes.get(name).copied())
582                == Some(scope);
583            if same_scope {
584                fiber.refresh();
585                refreshed.push(fiber);
586            }
587        }
588
589        if let Some(root) = self.root_fiber.get().and_then(Fiber::context) {
590            let mut args = vec![Value::new(name.to_owned())];
591            if let Ok(Some(value)) = self.reflect.value(&root, name, false) {
592                args.push(value);
593            }
594            if let Err(error) = root.events().emit("internal/service", args) {
595                root.log_error(error);
596            }
597        }
598        refreshed
599    }
600
601    pub(crate) fn notify_fiber_services(&self, fiber: &Fiber) {
602        let Some(uid) = fiber.uid() else {
603            return;
604        };
605        for (name, scope) in self.reflect.services_owned_by(uid) {
606            self.notify_service(&name, scope);
607        }
608    }
609}