Skip to main content

pax_runtime/
store.rs

1use crate::{api::NodeContext, ExpandedNode};
2use pax_runtime_api::{use_RefCell, Store};
3use std::{
4    any::{type_name, Any, TypeId},
5    cell::Cell,
6    collections::HashMap,
7    rc::Rc,
8};
9
10use_RefCell!();
11
12pub use pax_runtime_api::store::StoreError;
13
14type StoreEntry = Rc<RefCell<Box<dyn Any>>>;
15
16#[derive(Clone, Copy, PartialEq)]
17enum ScopeState {
18    Pending,
19    Mounted,
20    Closed,
21}
22
23#[derive(Clone)]
24pub(crate) struct NodeStores {
25    entries: RefCell<HashMap<TypeId, StoreEntry>>,
26    generation: Cell<u64>,
27    parent_generation: Cell<Option<u64>>,
28    state: Cell<ScopeState>,
29}
30
31impl NodeStores {
32    pub(crate) fn new(parent: Option<&ExpandedNode>) -> Self {
33        Self {
34            entries: RefCell::new(HashMap::new()),
35            generation: Cell::new(1),
36            parent_generation: Cell::new(parent.map(|p| p.stores.generation())),
37            state: Cell::new(ScopeState::Pending),
38        }
39    }
40
41    pub(crate) fn generation(&self) -> u64 {
42        self.generation.get()
43    }
44    pub(crate) fn is_closed(&self) -> bool {
45        self.state.get() == ScopeState::Closed
46    }
47    pub(crate) fn has_providers(&self) -> bool {
48        !self.entries.borrow().is_empty()
49    }
50
51    pub(crate) fn mount(&self, parent: Option<&ExpandedNode>) {
52        if self.is_closed() {
53            self.generation.set(
54                self.generation
55                    .get()
56                    .checked_add(1)
57                    .expect("store generation exhausted"),
58            );
59            self.parent_generation
60                .set(parent.map(|p| p.stores.generation()));
61        }
62        self.state.set(ScopeState::Mounted);
63    }
64
65    pub(crate) fn close(&self) {
66        self.state.set(ScopeState::Closed);
67        self.generation.set(
68            self.generation
69                .get()
70                .checked_add(1)
71                .expect("store generation exhausted"),
72        );
73        // Drop user values after releasing the registry borrow.
74        let entries = std::mem::take(&mut *self.entries.borrow_mut());
75        drop(entries);
76    }
77
78    fn check(&self, generation: u64) -> Result<(), StoreError> {
79        if self.generation() != generation || self.state.get() != ScopeState::Mounted {
80            Err(StoreError::ExpiredScope)
81        } else {
82            Ok(())
83        }
84    }
85}
86
87impl NodeContext {
88    /// Publish a store on this context's mounted node, keyed by its concrete Rust type.
89    ///
90    /// Register in the component's own mount handler to give each component
91    /// instance an independent provider. In an inline descendant handler, the
92    /// context identifies that descendant even when Rust `self` is its authoring
93    /// component. The context's node, not `self`, owns the registration.
94    ///
95    /// The owner and its logical descendants can find the value with `with_store`.
96    /// This includes private template children and caller-supplied children;
97    /// private wrappers around a slot do not change projected content's ancestry.
98    /// Siblings and ancestors cannot discover this registration.
99    ///
100    /// Providing the same type again on the same owner replaces its entry for
101    /// future lookups. Previously cloned handles keep their original state; the
102    /// registry is not reactive. Prefer updating properties in a stable store.
103    /// Providers are cleared at final unmount, after unmount handlers run.
104    ///
105    /// Returns `StoreError::ExpiredScope` for an unmounted or stale context, or
106    /// `StoreError::BorrowConflict` if the entry being replaced is borrowed.
107    pub fn provide_store<T: Store>(&self, store: T) -> Result<(), StoreError> {
108        let node = self
109            .expanded_node
110            .upgrade()
111            .ok_or(StoreError::ExpiredScope)?;
112        node.stores.check(self.store_generation)?;
113        let key = TypeId::of::<T>();
114        let old = node.stores.entries.borrow().get(&key).cloned();
115        if let Some(old) = &old {
116            old.try_borrow_mut()
117                .map_err(|_| StoreError::BorrowConflict {
118                    type_name: type_name::<T>(),
119                })?;
120        }
121        let replaced = node
122            .stores
123            .entries
124            .borrow_mut()
125            .insert(key, Rc::new(RefCell::new(Box::new(store))));
126        drop(replaced);
127        Ok(())
128    }
129
130    /// Borrow the nearest provider of `T`, starting with this context's node.
131    ///
132    /// Lookup follows logical template ancestry, independently of expression
133    /// scope and render parents. A nearer same-type provider shadows an outer
134    /// one; unrelated types remain visible. It never searches siblings or
135    /// descendants. Two consumers share state when they resolve the same provider.
136    ///
137    /// The closure exclusively borrows that store for its duration. Clone a
138    /// needed `Property` handle inside the closure, then read or update it after
139    /// the borrow ends. This avoids conflicts if subsequent work looks up the
140    /// same store. The returned value can be a handle or an owned snapshot, but
141    /// cannot borrow from the store.
142    ///
143    /// Returns `StoreError::Missing` when no provider exists,
144    /// `StoreError::ExpiredScope` when the context or an ancestor's mount has
145    /// ended, and `StoreError::BorrowConflict` when the nearest provider is
146    /// already borrowed. A conflict or expired scope never falls back to an
147    /// outer provider. Saved contexts cannot discover a later mount's stores.
148    pub fn with_store<T: Store, V>(&self, f: impl FnOnce(&mut T) -> V) -> Result<V, StoreError> {
149        let mut node = self
150            .expanded_node
151            .upgrade()
152            .ok_or(StoreError::ExpiredScope)?;
153        node.stores.check(self.store_generation)?;
154        let key = TypeId::of::<T>();
155        loop {
156            let entry = node.stores.entries.borrow().get(&key).cloned();
157            if let Some(entry) = entry {
158                let mut value = entry
159                    .try_borrow_mut()
160                    .map_err(|_| StoreError::BorrowConflict {
161                        type_name: type_name::<T>(),
162                    })?;
163                return Ok(f(value
164                    .downcast_mut::<T>()
165                    .expect("store key matches value type")));
166            }
167            let Some(generation) = node.stores.parent_generation.get() else {
168                return Err(StoreError::Missing {
169                    type_name: type_name::<T>(),
170                });
171            };
172            let parent = node
173                .template_parent
174                .upgrade()
175                .ok_or(StoreError::ExpiredScope)?;
176            // Projection can flatten never-mounted control-flow nodes. Their
177            // pending scopes remain transparent, but a closed generation cannot
178            // reconnect a retained child to a new provider after remount.
179            if parent.stores.generation() != generation || parent.stores.is_closed() {
180                return Err(StoreError::ExpiredScope);
181            }
182            node = parent;
183        }
184    }
185}