Skip to main content

incular_core/
context.rs

1//! Build contexts, inherited values, and dependency-aware signals.
2//!
3//! The context is deliberately scheduler agnostic. A runtime can associate a
4//! [`ConsumerId`] with an element and use [`BuildContext::take_dirty`] to
5//! enqueue it; core only records dependencies and reports invalidation.
6
7use std::{
8    any::{Any, TypeId},
9    cell::{Cell, RefCell},
10    collections::{HashMap, HashSet},
11    fmt,
12    rc::{Rc, Weak},
13    sync::atomic::{AtomicU64, Ordering},
14};
15
16static NEXT_CONSUMER: AtomicU64 = AtomicU64::new(1);
17static NEXT_ENVIRONMENT: AtomicU64 = AtomicU64::new(1);
18use crate::reactivity::{DependencySource, Subscription, TrackingGuard};
19
20fn next_identity(counter: &AtomicU64, label: &'static str) -> u64 {
21    counter
22        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
23        .unwrap_or_else(|_| panic!("{label} identity space exhausted"))
24}
25
26fn next_revision(revision: u64) -> u64 {
27    revision
28        .checked_add(1)
29        .expect("context dependency revision exhausted")
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
33enum DependencyKey {
34    Environment { environment: u64, type_id: TypeId },
35}
36
37#[derive(Default)]
38struct TrackerInner {
39    dependencies: RefCell<HashMap<ConsumerId, HashMap<u64, Subscription>>>,
40    sources: RefCell<HashMap<DependencyKey, DependencySource>>,
41    /// Environment source keys by source id, so clearing a consumer prunes
42    /// only the sources it observed instead of scanning all of them.
43    source_keys: RefCell<HashMap<u64, DependencyKey>>,
44    dirty: RefCell<HashSet<ConsumerId>>,
45    owners: RefCell<HashMap<ConsumerId, Weak<ConsumerOwner>>>,
46}
47impl TrackerInner {
48    fn owner(self: &Rc<Self>, consumer: ConsumerId) -> Rc<ConsumerOwner> {
49        let mut owners = self.owners.borrow_mut();
50        if let Some(owner) = owners.get(&consumer).and_then(Weak::upgrade) {
51            return owner;
52        }
53        let owner = Rc::new(ConsumerOwner {
54            tracker: Rc::downgrade(self),
55            consumer,
56        });
57        owners.insert(consumer, Rc::downgrade(&owner));
58        owner
59    }
60
61    fn record(self: &Rc<Self>, consumer: ConsumerId, dependency: DependencyKey) {
62        let source = self
63            .sources
64            .borrow_mut()
65            .entry(dependency)
66            .or_insert_with(|| {
67                let source = DependencySource::default();
68                self.source_keys
69                    .borrow_mut()
70                    .insert(source.id(), dependency);
71                source
72            })
73            .clone();
74        self.record_source(consumer, source);
75    }
76    fn record_source(self: &Rc<Self>, consumer: ConsumerId, source: DependencySource) {
77        let mut dependencies = self.dependencies.borrow_mut();
78        dependencies
79            .entry(consumer)
80            .or_default()
81            .entry(source.id())
82            .or_insert_with(|| {
83                let weak = Rc::downgrade(self);
84                source.subscribe(consumer, move || {
85                    if let Some(tracker) = weak.upgrade() {
86                        tracker.dirty.borrow_mut().insert(consumer);
87                    }
88                })
89            });
90    }
91    fn clear(&self, consumer: ConsumerId) {
92        self.dirty.borrow_mut().remove(&consumer);
93        let Some(subscriptions) = self.dependencies.borrow_mut().remove(&consumer) else {
94            return;
95        };
96        let ids: Vec<u64> = subscriptions.keys().copied().collect();
97        // Dropping the subscriptions unsubscribes them; an environment
98        // source nobody observes any more is then released.
99        drop(subscriptions);
100        let mut sources = self.sources.borrow_mut();
101        let mut keys = self.source_keys.borrow_mut();
102        for id in ids {
103            if let Some(key) = keys.get(&id).copied()
104                && sources
105                    .get(&key)
106                    .is_some_and(|source| source.subscriber_count() == 0)
107            {
108                sources.remove(&key);
109                keys.remove(&id);
110            }
111        }
112    }
113    fn invalidate(&self, dependency: DependencyKey) {
114        let source = self.sources.borrow().get(&dependency).cloned();
115        if let Some(source) = source {
116            source.notify();
117        }
118    }
119    fn dependency_count(&self, consumer: ConsumerId) -> usize {
120        self.dependencies
121            .borrow()
122            .get(&consumer)
123            .map_or(0, HashMap::len)
124    }
125    fn is_dirty(&self, consumer: ConsumerId) -> bool {
126        self.dirty.borrow().contains(&consumer)
127    }
128    fn take_dirty(&self, consumer: ConsumerId) -> bool {
129        self.dirty.borrow_mut().remove(&consumer)
130    }
131    fn take_all_dirty(&self) -> Vec<ConsumerId> {
132        self.dirty.borrow_mut().drain().collect()
133    }
134}
135
136struct ConsumerOwner {
137    tracker: Weak<TrackerInner>,
138    consumer: ConsumerId,
139}
140impl Drop for ConsumerOwner {
141    fn drop(&mut self) {
142        if let Some(tracker) = self.tracker.upgrade() {
143            tracker.clear(self.consumer);
144            tracker.owners.borrow_mut().remove(&self.consumer);
145        }
146    }
147}
148
149struct Environment {
150    id: u64,
151    parent: Option<Rc<Environment>>,
152    values: RefCell<HashMap<TypeId, Rc<dyn Any>>>,
153    revision: Cell<u64>,
154}
155
156impl Environment {
157    fn root() -> Rc<Self> {
158        Rc::new(Self {
159            id: next_identity(&NEXT_ENVIRONMENT, "environment"),
160            parent: None,
161            values: RefCell::new(HashMap::new()),
162            revision: Cell::new(0),
163        })
164    }
165
166    fn child(parent: &Rc<Self>) -> Rc<Self> {
167        Rc::new(Self {
168            id: next_identity(&NEXT_ENVIRONMENT, "environment"),
169            parent: Some(parent.clone()),
170            values: RefCell::new(HashMap::new()),
171            revision: Cell::new(0),
172        })
173    }
174
175    fn lookup(&self, type_id: TypeId) -> Option<(u64, Rc<dyn Any>)> {
176        if let Some(value) = self.values.borrow().get(&type_id) {
177            return Some((self.id, value.clone()));
178        }
179        self.parent.as_deref()?.lookup(type_id)
180    }
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
184pub struct ConsumerId(u64);
185
186impl ConsumerId {
187    #[must_use]
188    pub fn new() -> Self {
189        Self(next_identity(&NEXT_CONSUMER, "context consumer"))
190    }
191
192    #[must_use]
193    pub const fn from_raw(value: u64) -> Self {
194        Self(value)
195    }
196
197    #[must_use]
198    pub const fn raw(self) -> u64 {
199        self.0
200    }
201}
202
203impl Default for ConsumerId {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// A read-only summary of the dependencies currently collected for a context.
210#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
211pub struct DependencySnapshot {
212    count: usize,
213    dirty: bool,
214}
215
216impl DependencySnapshot {
217    #[must_use]
218    pub const fn count(self) -> usize {
219        self.count
220    }
221
222    #[must_use]
223    pub const fn is_empty(self) -> bool {
224        self.count == 0
225    }
226
227    #[must_use]
228    pub const fn is_dirty(self) -> bool {
229        self.dirty
230    }
231}
232
233struct ActiveScope {
234    tracker: Rc<TrackerInner>,
235    consumer: ConsumerId,
236}
237
238thread_local! {
239    static ACTIVE_SCOPES: RefCell<Vec<ActiveScope>> = const { RefCell::new(Vec::new()) };
240}
241
242/// A scoped build context. Cloning a context keeps the same consumer identity;
243/// [`BuildContext::for_consumer`] creates a distinct dependency owner.
244#[derive(Clone)]
245pub struct BuildContext {
246    _owner: Rc<ConsumerOwner>,
247    environment: Rc<Environment>,
248    tracker: Rc<TrackerInner>,
249    consumer: ConsumerId,
250}
251
252impl fmt::Debug for BuildContext {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.debug_struct("BuildContext")
255            .field("consumer", &self.consumer)
256            .field("dependency_count", &self.dependency_count())
257            .field("dirty", &self.is_dirty())
258            .finish()
259    }
260}
261
262impl Default for BuildContext {
263    fn default() -> Self {
264        Self::new()
265    }
266}
267
268impl BuildContext {
269    /// Creates an empty root context with a fresh dependency consumer.
270    #[must_use]
271    pub fn new() -> Self {
272        Self::for_consumer(ConsumerId::new())
273    }
274
275    /// Creates an empty root context for a caller-owned consumer identity.
276    #[must_use]
277    pub fn for_consumer(consumer: ConsumerId) -> Self {
278        let tracker = Rc::new(TrackerInner::default());
279        Self {
280            _owner: tracker.owner(consumer),
281            environment: Environment::root(),
282            tracker,
283            consumer,
284        }
285    }
286
287    /// Creates another dependency owner that observes the same retained
288    /// inherited environment and shares this context's dependency tracker.
289    #[must_use]
290    pub fn for_related_consumer(&self, consumer: ConsumerId) -> Self {
291        Self {
292            _owner: self.tracker.owner(consumer),
293            environment: self.environment.clone(),
294            tracker: self.tracker.clone(),
295            consumer,
296        }
297    }
298
299    /// Creates an explicit inherited lookup boundary for another consumer.
300    /// Values above this point are not visible, while dependency invalidation
301    /// remains in the same retained-tree tracker.
302    #[must_use]
303    pub fn boundary_for_consumer(&self, consumer: ConsumerId) -> Self {
304        Self {
305            _owner: self.tracker.owner(consumer),
306            environment: Environment::root(),
307            tracker: self.tracker.clone(),
308            consumer,
309        }
310    }
311
312    /// Returns the identity used for dependency invalidation.
313    #[must_use]
314    pub const fn consumer_id(&self) -> ConsumerId {
315        self.consumer
316    }
317
318    /// Creates a child environment. Values in the child shadow values in its
319    /// parent while keeping the same dependency tracker and consumer.
320    #[must_use]
321    pub fn child(&self) -> Self {
322        Self {
323            _owner: self._owner.clone(),
324            environment: Environment::child(&self.environment),
325            tracker: self.tracker.clone(),
326            consumer: self.consumer,
327        }
328    }
329
330    /// Creates a child context with one inherited value.
331    #[must_use]
332    pub fn provide<T: Any>(&self, value: T) -> Self {
333        let child = self.child();
334        child.insert(value);
335        child
336    }
337
338    /// Creates a child environment from a type-erased framework value. This is
339    /// the retained-tree integration point; application-facing APIs should use
340    /// typed `provide`/lookup methods instead.
341    #[doc(hidden)]
342    #[must_use]
343    pub fn provide_erased(&self, type_id: TypeId, value: Rc<dyn Any>) -> Self {
344        let child = self.child();
345        child.environment.values.borrow_mut().insert(type_id, value);
346        child
347    }
348
349    /// Replaces a local type-erased value and invalidates only consumers that
350    /// subscribed to this exact retained environment/type key.
351    #[doc(hidden)]
352    pub fn set_erased(&self, type_id: TypeId, value: Rc<dyn Any>) {
353        self.environment.values.borrow_mut().insert(type_id, value);
354        self.environment
355            .revision
356            .set(next_revision(self.environment.revision.get()));
357        self.tracker.invalidate(DependencyKey::Environment {
358            environment: self.environment.id,
359            type_id,
360        });
361    }
362
363    /// Inserts or replaces a value in this environment. Existing consumers
364    /// that watched this type are marked dirty when the value changes.
365    pub fn insert<T: Any>(&self, value: T) -> bool {
366        let type_id = TypeId::of::<T>();
367        self.environment
368            .values
369            .borrow_mut()
370            .insert(type_id, Rc::new(value));
371        self.environment
372            .revision
373            .set(next_revision(self.environment.revision.get()));
374        self.tracker.invalidate(DependencyKey::Environment {
375            environment: self.environment.id,
376            type_id,
377        });
378        true
379    }
380
381    /// Replaces a value only when its `PartialEq` value changed.
382    pub fn set<T: Any + PartialEq>(&self, value: T) -> bool {
383        let type_id = TypeId::of::<T>();
384        let changed = self
385            .environment
386            .values
387            .borrow()
388            .get(&type_id)
389            .and_then(|old| old.downcast_ref::<T>())
390            .is_none_or(|old| old != &value);
391        if changed {
392            self.insert(value);
393        }
394        changed
395    }
396
397    /// Removes a value from this environment.
398    pub fn remove<T: Any>(&self) -> bool {
399        let type_id = TypeId::of::<T>();
400        let removed = self
401            .environment
402            .values
403            .borrow_mut()
404            .remove(&type_id)
405            .is_some();
406        if removed {
407            self.environment
408                .revision
409                .set(next_revision(self.environment.revision.get()));
410            self.tracker.invalidate(DependencyKey::Environment {
411                environment: self.environment.id,
412                type_id,
413            });
414        }
415        removed
416    }
417
418    #[must_use]
419    pub fn contains<T: Any>(&self) -> bool {
420        self.lookup_value(TypeId::of::<T>()).is_some()
421    }
422
423    #[must_use]
424    pub fn revision(&self) -> u64 {
425        self.environment.revision.get()
426    }
427
428    fn lookup_value(&self, type_id: TypeId) -> Option<(u64, Rc<dyn Any>)> {
429        self.environment.lookup(type_id)
430    }
431
432    /// Reads an inherited value without recording a dependency.
433    #[must_use]
434    pub fn read<T: Any + Clone>(&self) -> Option<T> {
435        self.read_shared::<T>().map(|value| (*value).clone())
436    }
437
438    /// Alias for [`BuildContext::read`].
439    #[must_use]
440    pub fn get<T: Any + Clone>(&self) -> Option<T> {
441        self.read()
442    }
443
444    /// Reads an inherited value as shared ownership without recording a
445    /// dependency.
446    #[must_use]
447    pub fn read_shared<T: Any>(&self) -> Option<Rc<T>> {
448        self.lookup_value(TypeId::of::<T>())
449            .and_then(|(_, value)| value.downcast::<T>().ok())
450    }
451
452    fn record_environment(&self, type_id: TypeId) {
453        let environment = self
454            .lookup_value(type_id)
455            .map_or(self.environment.id, |(environment, _)| environment);
456        with_active_scope(|scope| {
457            scope.tracker.record(
458                scope.consumer,
459                DependencyKey::Environment {
460                    environment,
461                    type_id,
462                },
463            );
464        })
465        .unwrap_or_else(|| {
466            self.tracker.record(
467                self.consumer,
468                DependencyKey::Environment {
469                    environment,
470                    type_id,
471                },
472            )
473        });
474    }
475
476    /// Reads a value and records the environment lookup as a dependency.
477    #[must_use]
478    pub fn depend<T: Any + Clone>(&self) -> Option<T> {
479        self.record_environment(TypeId::of::<T>());
480        self.read()
481    }
482
483    /// Reads a shared inherited value while recording the dependency without
484    /// cloning the value itself.
485    #[must_use]
486    pub fn depend_shared<T: Any>(&self) -> Option<Rc<T>> {
487        self.record_environment(TypeId::of::<T>());
488        self.read_shared()
489    }
490
491    /// Alias for [`BuildContext::depend`], matching reactive UI terminology.
492    #[must_use]
493    pub fn watch<T: Any + Clone>(&self) -> Option<T> {
494        self.depend()
495    }
496
497    /// Enters this context as the active build scope. Signal reads made while
498    /// the guard is alive are associated with this context's consumer.
499    #[must_use]
500    pub fn enter(&self) -> ContextGuard {
501        ACTIVE_SCOPES.with(|scopes| {
502            scopes.borrow_mut().push(ActiveScope {
503                tracker: self.tracker.clone(),
504                consumer: self.consumer,
505            });
506        });
507        let tracker = self.tracker.clone();
508        let consumer = self.consumer;
509        ContextGuard {
510            active: true,
511            _owner: self._owner.clone(),
512            _read_only: crate::reactivity::ReadOnlyGuard::enter(),
513            _tracking: TrackingGuard::enter(move |source| tracker.record_source(consumer, source)),
514        }
515    }
516
517    /// Alias for [`BuildContext::enter`].
518    #[must_use]
519    pub fn scope(&self) -> ContextGuard {
520        self.enter()
521    }
522
523    /// Clears old dependencies, enters the build scope, and invokes the
524    /// callback. This is the normal runtime integration point.
525    pub fn build<R>(&self, callback: impl FnOnce(&BuildContext) -> R) -> R {
526        self.clear_dependencies();
527        let _guard = self.enter();
528        callback(self)
529    }
530
531    /// Runs a closure in a fresh dependency collection. Use [`BuildContext::build`]
532    /// when the callback needs the context as an argument.
533    pub fn run<R>(&self, callback: impl FnOnce() -> R) -> R {
534        self.clear_dependencies();
535        let _guard = self.enter();
536        callback()
537    }
538
539    /// Alias for [`BuildContext::build`].
540    pub fn with_context<R>(&self, callback: impl FnOnce(&BuildContext) -> R) -> R {
541        self.build(callback)
542    }
543
544    pub fn clear_dependencies(&self) {
545        self.tracker.clear(self.consumer);
546    }
547
548    #[must_use]
549    pub fn dependency_count(&self) -> usize {
550        self.tracker.dependency_count(self.consumer)
551    }
552
553    #[must_use]
554    pub fn is_dirty(&self) -> bool {
555        self.tracker.is_dirty(self.consumer)
556    }
557
558    /// Takes and clears this consumer's invalidation bit.
559    pub fn take_dirty(&self) -> bool {
560        self.tracker.take_dirty(self.consumer)
561    }
562
563    /// Drains all dirty consumers that share this retained dependency tracker.
564    /// Schedulers use this to translate precise inherited-value invalidations
565    /// into their own element work queues.
566    #[doc(hidden)]
567    pub fn take_dirty_consumers(&self) -> Vec<ConsumerId> {
568        self.tracker.take_all_dirty()
569    }
570
571    /// Whether this retained dependency tracker currently has invalidated
572    /// consumers. Framework schedulers use this to request a frame without
573    /// consuming the identities that reconciliation still needs to drain.
574    #[doc(hidden)]
575    #[must_use]
576    pub fn has_dirty_consumers(&self) -> bool {
577        !self.tracker.dirty.borrow().is_empty()
578    }
579
580    #[must_use]
581    pub fn dependencies(&self) -> DependencySnapshot {
582        DependencySnapshot {
583            count: self.dependency_count(),
584            dirty: self.is_dirty(),
585        }
586    }
587}
588
589/// Restores the previous active build scope when dropped.
590pub struct ContextGuard {
591    active: bool,
592    _owner: Rc<ConsumerOwner>,
593    _tracking: TrackingGuard,
594    _read_only: crate::reactivity::ReadOnlyGuard,
595}
596
597impl Drop for ContextGuard {
598    fn drop(&mut self) {
599        if self.active {
600            ACTIVE_SCOPES.with(|scopes| {
601                let _ = scopes.borrow_mut().pop();
602            });
603            self.active = false;
604        }
605    }
606}
607
608fn with_active_scope<R>(callback: impl FnOnce(&ActiveScope) -> R) -> Option<R> {
609    ACTIVE_SCOPES.with(|scopes| scopes.borrow().last().map(callback))
610}