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