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}