Skip to main content

pax_runtime_api/properties/
shared_graph.rs

1use super::{
2    properties_table::PROPERTY_TABLE,
3    shared_property::{lock, next_id, SharedCell},
4    LocalProperty, Property, SharedPropertyValue,
5};
6use std::{
7    any::Any,
8    cell::{Cell, RefCell},
9    collections::{HashMap, HashSet, VecDeque},
10    rc::Rc,
11    sync::{Arc, Mutex, Weak},
12};
13
14#[derive(Default)]
15struct InboxState {
16    attached: HashSet<u64>,
17    pending: HashSet<u64>,
18    order: VecDeque<u64>,
19    closed: bool,
20}
21
22pub(super) struct GraphInbox {
23    state: Mutex<InboxState>,
24    wake: Mutex<Arc<dyn Fn() + Send + Sync>>,
25}
26
27impl GraphInbox {
28    pub fn publish(&self, id: u64) {
29        let wake = {
30            let mut state = lock(&self.state);
31            if state.closed || !state.attached.contains(&id) {
32                return;
33            }
34            let wake = state.pending.is_empty();
35            if state.pending.insert(id) {
36                state.order.push_back(id);
37            }
38            wake
39        };
40        if wake {
41            self.wake();
42        }
43    }
44
45    fn wake(&self) {
46        let wake = lock(&self.wake).clone();
47        wake();
48    }
49
50    fn attach(&self, id: u64) {
51        lock(&self.state).attached.insert(id);
52    }
53
54    fn detach(&self, id: u64) {
55        let mut state = lock(&self.state);
56        state.attached.remove(&id);
57        state.pending.remove(&id);
58        state.order.retain(|queued| *queued != id);
59    }
60}
61
62thread_local! {
63    static ACTIVE_GRAPH: RefCell<Option<PropertyGraph>> = const { RefCell::new(None) };
64    static ACTIVE_SCOPE: RefCell<Option<std::rc::Weak<BindingScopeInner>>> = const { RefCell::new(None) };
65}
66
67pub(super) fn with_current_graph<R>(f: impl FnOnce(&PropertyGraph) -> R) -> R {
68    let graph = ACTIVE_GRAPH
69        .with(|active| active.borrow().clone())
70        .expect("shared property graph access requires PropertyGraph::enter on its owner thread");
71    f(&graph)
72}
73
74pub(super) fn current_graph_id() -> Option<u64> {
75    ACTIVE_GRAPH.with(|active| active.borrow().as_ref().map(|graph| graph.inner.id))
76}
77
78pub(super) fn current_binding_scope() -> Option<std::rc::Weak<BindingScopeInner>> {
79    ACTIVE_SCOPE.with(|active| active.borrow().clone())
80}
81
82// A local evaluator belongs to the graph that created it, even if another
83// application on the same UI thread happens to drain the shared effect queue.
84pub(super) fn owner_evaluator<T: 'static>(evaluator: impl Fn() -> T + 'static) -> impl Fn() -> T {
85    let owner = ACTIVE_GRAPH.with(|active| {
86        active
87            .borrow()
88            .as_ref()
89            .map(|graph| Rc::downgrade(&graph.inner))
90    });
91    let scope = ACTIVE_SCOPE.with(|active| active.borrow().clone());
92    move || {
93        let graph = owner.as_ref().map(|owner| PropertyGraph {
94            inner: owner
95                .upgrade()
96                .expect("cannot evaluate a binding after its graph has been destroyed"),
97        });
98        let _entered = graph.as_ref().map(PropertyGraph::enter);
99        // Pure local dependencies may still be read during reconciliation or
100        // by another mounted view. Restore their attachment context without
101        // reviving it; attach() treats a closed/missing owner as detached.
102        let previous = ACTIVE_SCOPE.with(|active| active.replace(scope.clone()));
103        let _scope = BindingScopeGuard { previous };
104        evaluator()
105    }
106}
107
108trait Attachment {
109    fn as_any(&self) -> &dyn Any;
110    fn prepare_import(&self) -> Option<Box<dyn FnOnce()>>;
111    fn live(&self) -> bool;
112    fn dormant(&self) -> Box<dyn DormantAttachment>;
113}
114
115trait DormantAttachment {
116    fn live(&self) -> bool;
117    fn restore(&self, graph: &PropertyGraph);
118    fn local_id(&self) -> super::private::PropertyId;
119}
120
121struct Dormant<T: SharedPropertyValue> {
122    shared: Weak<SharedCell<T>>,
123    local: super::private::PropertyId,
124}
125
126impl<T: SharedPropertyValue> DormantAttachment for Dormant<T> {
127    fn local_id(&self) -> super::private::PropertyId {
128        self.local
129    }
130    fn live(&self) -> bool {
131        self.shared.strong_count() != 0
132            && PROPERTY_TABLE.with(|table| table.has_live_entry(self.local))
133    }
134    fn restore(&self, graph: &PropertyGraph) {
135        let Some(cell) = self.shared.upgrade() else {
136            return;
137        };
138        let Some(untyped) = super::UntypedProperty::try_from_id(self.local) else {
139            return;
140        };
141        let local = LocalProperty::new_from_untyped(untyped);
142        let property = Property { cell };
143        graph.inner.inbox.attach(property.cell.id);
144        property.cell.subscribe(graph.inner.id, &graph.inner.inbox);
145        let snapshot = property.cell.snapshot();
146        PROPERTY_TABLE.with(|table| table.import_shared(self.local, (*snapshot.value).clone()));
147        PROPERTY_TABLE.with(|table| table.invalidate(self.local));
148        graph.insert(&property, local, snapshot.revision);
149    }
150}
151
152struct TypedAttachment<T: SharedPropertyValue> {
153    graph: u64,
154    shared: Weak<SharedCell<T>>,
155    local: LocalProperty<T>,
156    revision: Rc<Cell<u64>>,
157    _publication_effect: LocalProperty<()>,
158}
159
160impl<T: SharedPropertyValue> Attachment for TypedAttachment<T> {
161    fn as_any(&self) -> &dyn Any {
162        self
163    }
164    fn live(&self) -> bool {
165        self.shared.strong_count() != 0
166    }
167    fn dormant(&self) -> Box<dyn DormantAttachment> {
168        Box::new(Dormant::<T> {
169            shared: self.shared.clone(),
170            local: self.local.untyped.get_id(),
171        })
172    }
173    fn prepare_import(&self) -> Option<Box<dyn FnOnce()>> {
174        let snapshot = self.shared.upgrade()?.snapshot();
175        if snapshot.revision <= self.revision.get() {
176            return None;
177        }
178        let local = self.local.clone();
179        let revision = self.revision.clone();
180        Some(Box::new(move || {
181            PROPERTY_TABLE.with(|table| {
182                table.import_shared(local.untyped.get_id(), (*snapshot.value).clone())
183            });
184            revision.set(snapshot.revision);
185        }))
186    }
187}
188
189impl<T: SharedPropertyValue> Drop for TypedAttachment<T> {
190    fn drop(&mut self) {
191        if let Some(shared) = self.shared.upgrade() {
192            shared.unsubscribe(self.graph);
193        }
194        let _ =
195            PROPERTY_TABLE.try_with(|table| table.clear_shared::<T>(self.local.untyped.get_id()));
196    }
197}
198
199struct GraphInner {
200    id: u64,
201    inbox: Arc<GraphInbox>,
202    attachments: RefCell<HashMap<u64, Box<dyn Attachment>>>,
203    dormant: RefCell<HashMap<u64, Box<dyn DormantAttachment>>>,
204    owners: RefCell<HashMap<u64, HashSet<u64>>>,
205    scopes: RefCell<HashMap<u64, std::rc::Weak<BindingScopeInner>>>,
206    closed: Cell<bool>,
207}
208
209impl Drop for GraphInner {
210    fn drop(&mut self) {
211        let mut state = lock(&self.inbox.state);
212        state.closed = true;
213        state.attached.clear();
214        state.pending.clear();
215        state.order.clear();
216    }
217}
218
219/// Owner-thread bridge between shared values and Pax's local reactive graph.
220///
221/// The wake callback may run on any publishing thread. It must schedule host
222/// work, never enter the engine inline. Enter this graph while constructing
223/// bindings; call [`Self::import`] at a safe UI update boundary before draining
224/// local effects. Imports coalesce by property and freeze a snapshot before any
225/// local evaluator runs. Graphs and their local views cannot cross threads.
226#[derive(Clone)]
227pub struct PropertyGraph {
228    inner: Rc<GraphInner>,
229}
230
231impl PropertyGraph {
232    /// Creates a bridge with a host-owned, transferable wake callback.
233    pub fn new(wake: impl Fn() + Send + Sync + 'static) -> Self {
234        Self {
235            inner: Rc::new(GraphInner {
236                id: next_id(),
237                inbox: Arc::new(GraphInbox {
238                    state: Mutex::new(InboxState::default()),
239                    wake: Mutex::new(Arc::new(wake)),
240                }),
241                attachments: RefCell::new(HashMap::new()),
242                dormant: RefCell::new(HashMap::new()),
243                owners: RefCell::new(HashMap::new()),
244                scopes: RefCell::new(HashMap::new()),
245                closed: Cell::new(false),
246            }),
247        }
248    }
249
250    /// Unique attachment identity for host scheduling. It never identifies an
251    /// engine pointer and is not reused after this graph closes.
252    pub fn id(&self) -> u64 {
253        self.inner.id
254    }
255
256    /// Whether publications still need an owner-thread import. Hosts recheck
257    /// this after a queued wake because an intervening frame may settle them.
258    pub fn has_pending(&self) -> bool {
259        let state = lock(&self.inner.inbox.state);
260        !state.closed && !state.pending.is_empty()
261    }
262
263    /// Installs the host scheduler after construction and wakes it if work
264    /// arrived before registration. The callback must only schedule work.
265    pub fn set_waker(&self, wake: impl Fn() + Send + Sync + 'static) {
266        *lock(&self.inner.inbox.wake) = Arc::new(wake);
267        let pending = {
268            let state = lock(&self.inner.inbox.state);
269            !state.closed && !state.pending.is_empty()
270        };
271        if pending {
272            self.inner.inbox.wake();
273        }
274    }
275
276    /// Selects this graph for shared-to-local bindings on this thread.
277    pub fn enter(&self) -> PropertyGraphGuard {
278        assert!(!self.inner.closed.get(), "property graph is closed");
279        let previous = ACTIVE_GRAPH.with(|active| active.replace(Some(self.clone())));
280        let previous_scope = ACTIVE_SCOPE.with(|active| active.take());
281        PropertyGraphGuard {
282            previous,
283            previous_scope,
284        }
285    }
286
287    pub(super) fn attach<T: SharedPropertyValue>(
288        &self,
289        property: &Property<T>,
290    ) -> LocalProperty<T> {
291        assert!(!self.inner.closed.get(), "property graph is closed");
292        let detached = ACTIVE_SCOPE.with(|active| {
293            active
294                .borrow()
295                .as_ref()
296                .is_some_and(|scope| scope.upgrade().is_none_or(|scope| scope.closed.get()))
297        });
298        if detached {
299            // Retained local computations can read their last projection, but
300            // cannot re-subscribe a view after unmount. Raw data remains data.
301            if let Some(attachment) = self.inner.attachments.borrow().get(&property.cell.id) {
302                return attachment
303                    .as_any()
304                    .downcast_ref::<TypedAttachment<T>>()
305                    .unwrap()
306                    .local
307                    .clone();
308            }
309            if let Some(dormant) = self.inner.dormant.borrow().get(&property.cell.id) {
310                if let Some(local) = super::UntypedProperty::try_from_id(dormant.local_id()) {
311                    return LocalProperty::new_from_untyped(local);
312                }
313            }
314            return LocalProperty::new(property.get());
315        }
316        self.record_owner(property.cell.id);
317        self.restore(property.cell.id);
318        if let Some(attachment) = self.inner.attachments.borrow().get(&property.cell.id) {
319            return attachment
320                .as_any()
321                .downcast_ref::<TypedAttachment<T>>()
322                .expect("shared property type changed")
323                .local
324                .clone();
325        }
326        // Subscribe before sampling: a racing writer is either in the initial
327        // snapshot or leaves a pending notification for the next import.
328        self.inner.inbox.attach(property.cell.id);
329        property.cell.subscribe(self.inner.id, &self.inner.inbox);
330        let snapshot = property.cell.snapshot();
331        let local = LocalProperty::new((*snapshot.value).clone());
332        self.insert(property, local.clone(), snapshot.revision);
333        local
334    }
335
336    pub(super) fn observed_revision<T: SharedPropertyValue>(
337        &self,
338        property: &Property<T>,
339    ) -> Option<u64> {
340        self.inner
341            .attachments
342            .borrow()
343            .get(&property.cell.id)
344            .map(|entry| {
345                entry
346                    .as_any()
347                    .downcast_ref::<TypedAttachment<T>>()
348                    .expect("shared property type changed")
349                    .revision
350                    .get()
351            })
352    }
353
354    pub(super) fn share<T: SharedPropertyValue>(&self, local: LocalProperty<T>) -> Property<T> {
355        if let Some(cell) =
356            PROPERTY_TABLE.with(|table| table.shared_storage::<T>(local.untyped.get_id()))
357        {
358            self.record_owner(cell.id);
359            return Property { cell };
360        }
361        let property = Property::new(local.get());
362        self.record_owner(property.cell.id);
363        self.inner.inbox.attach(property.cell.id);
364        property.cell.subscribe(self.inner.id, &self.inner.inbox);
365        self.insert(&property, local, 0);
366        property
367    }
368
369    fn insert<T: SharedPropertyValue>(
370        &self,
371        property: &Property<T>,
372        local: LocalProperty<T>,
373        revision: u64,
374    ) {
375        let seen = Rc::new(Cell::new(revision));
376        let weak = Arc::downgrade(&property.cell);
377        let weak_writer = weak.clone();
378        let writer_seen = seen.clone();
379        let graph = self.inner.id;
380        PROPERTY_TABLE.with(|table| {
381            table.set_shared(
382                local.untyped.get_id(),
383                weak.clone(),
384                Rc::new(move |value: &T| {
385                    if let Some(shared) = weak_writer.upgrade() {
386                        writer_seen.set(shared.publish_local(value.clone(), graph));
387                    }
388                }),
389            )
390        });
391        // The projection belongs to every view that attaches it. Its eager
392        // publication effect must survive the first view's unmount.
393        let _graph = self.enter();
394        let observed = local.clone();
395        let effect = LocalProperty::computed(
396            move || {
397                observed.get();
398            },
399            &[local.untyped()],
400        );
401        super::register_effect_property(&effect);
402        self.inner.attachments.borrow_mut().insert(
403            property.cell.id,
404            Box::new(TypedAttachment {
405                graph,
406                shared: weak,
407                local,
408                revision: seen,
409                _publication_effect: effect,
410            }),
411        );
412    }
413
414    /// Imports a finite snapshot of pending publications without running user
415    /// evaluators. Publications racing this import stay pending for a later turn.
416    /// Returns the number of imported properties.
417    pub fn import(&self) -> usize {
418        self.import_batch(1024)
419    }
420
421    /// Imports at most `limit` queued identities in FIFO order and requests a
422    /// further host turn if work remains. Repeated writes occupy one slot.
423    pub fn import_batch(&self, limit: usize) -> usize {
424        if self.inner.closed.get() {
425            return 0;
426        }
427        let _entered = self.enter();
428        let pending: Vec<_> = {
429            let mut state = lock(&self.inner.inbox.state);
430            let count = limit.min(state.order.len());
431            let pending: Vec<_> = state.order.drain(..count).collect();
432            for id in &pending {
433                state.pending.remove(id);
434            }
435            pending
436        };
437        let actions: Vec<_> = {
438            let attachments = self.inner.attachments.borrow();
439            pending
440                .into_iter()
441                .filter_map(|id| attachments.get(&id)?.prepare_import())
442                .collect()
443        };
444        let count = actions.len();
445        for action in actions {
446            action();
447        }
448        self.collect();
449        if self.has_pending() {
450            self.inner.inbox.wake();
451        }
452        count
453    }
454
455    /// Releases local bindings whose shared allocation has no remaining owners.
456    pub fn collect(&self) {
457        self.inner
458            .dormant
459            .borrow_mut()
460            .retain(|_, attachment| attachment.live());
461        let dead: Vec<_> = self
462            .inner
463            .attachments
464            .borrow()
465            .iter()
466            .filter_map(|(id, attachment)| (!attachment.live()).then_some(*id))
467            .collect();
468        let retired: Vec<_> = {
469            let mut attachments = self.inner.attachments.borrow_mut();
470            dead.into_iter()
471                .filter_map(|id| {
472                    self.inner.inbox.detach(id);
473                    if let Some(owners) = self.inner.owners.borrow_mut().remove(&id) {
474                        let scopes = self.inner.scopes.borrow();
475                        for owner in owners {
476                            if let Some(scope) = scopes.get(&owner).and_then(std::rc::Weak::upgrade)
477                            {
478                                scope.properties.borrow_mut().remove(&id);
479                            }
480                        }
481                    }
482                    attachments.remove(&id)
483                })
484                .collect()
485        };
486        drop(retired);
487    }
488
489    /// Detaches every graph binding on the owner thread. Retained shared values
490    /// remain usable but can no longer wake or evaluate this graph.
491    #[doc(hidden)]
492    pub fn stop_publications(&self) {
493        let mut state = lock(&self.inner.inbox.state);
494        state.closed = true;
495        state.attached.clear();
496        state.pending.clear();
497        state.order.clear();
498    }
499
500    pub fn close(&self) {
501        self.inner.closed.set(true);
502        {
503            let mut state = lock(&self.inner.inbox.state);
504            state.closed = true;
505            state.attached.clear();
506            state.pending.clear();
507            state.order.clear();
508        }
509        let retired = std::mem::take(&mut *self.inner.attachments.borrow_mut());
510        self.inner.dormant.borrow_mut().clear();
511        self.inner.owners.borrow_mut().clear();
512        self.inner.scopes.borrow_mut().clear();
513        drop(retired);
514    }
515
516    /// Creates an internal owner for view bindings. Unmount suspends its graph
517    /// attachments while allowing retained shared data to survive independently.
518    #[doc(hidden)]
519    pub fn binding_scope(&self) -> BindingScope {
520        let scope = BindingScope {
521            inner: Rc::new(BindingScopeInner {
522                graph: Rc::downgrade(&self.inner),
523                id: next_id(),
524                closed: Cell::new(false),
525                properties: RefCell::new(HashSet::new()),
526            }),
527        };
528        self.inner
529            .scopes
530            .borrow_mut()
531            .insert(scope.inner.id, Rc::downgrade(&scope.inner));
532        scope
533    }
534
535    fn restore(&self, id: u64) {
536        let dormant = self.inner.dormant.borrow_mut().remove(&id);
537        if let Some(dormant) = dormant {
538            dormant.restore(self);
539        }
540    }
541
542    fn record_owner(&self, id: u64) {
543        let scope =
544            ACTIVE_SCOPE.with(|scope| scope.borrow().as_ref().and_then(std::rc::Weak::upgrade));
545        let mut owners = self.inner.owners.borrow_mut();
546        let owners = owners.entry(id).or_default();
547        if let Some(scope) = scope.filter(|scope| scope.graph.ptr_eq(&Rc::downgrade(&self.inner))) {
548            assert!(
549                !scope.closed.get(),
550                "cannot attach properties for an unmounted view"
551            );
552            scope.properties.borrow_mut().insert(id);
553            owners.insert(scope.id);
554        } else if owners.is_empty() {
555            owners.insert(0);
556        }
557    }
558
559    fn release_owner(&self, scope: &BindingScopeInner) {
560        let retired: Vec<_> = scope
561            .properties
562            .borrow()
563            .iter()
564            .filter_map(|id| {
565                let mut owners = self.inner.owners.borrow_mut();
566                let set = owners.get_mut(id)?;
567                set.remove(&scope.id);
568                if !set.is_empty() {
569                    return None;
570                }
571                owners.remove(id);
572                self.inner
573                    .attachments
574                    .borrow_mut()
575                    .remove(id)
576                    .map(|attachment| (*id, attachment))
577            })
578            .collect();
579        for (id, attachment) in retired {
580            self.inner
581                .dormant
582                .borrow_mut()
583                .insert(id, attachment.dormant());
584            self.inner.inbox.detach(id);
585            drop(attachment);
586        }
587    }
588}
589
590pub(super) struct BindingScopeInner {
591    graph: std::rc::Weak<GraphInner>,
592    id: u64,
593    pub(super) closed: Cell<bool>,
594    properties: RefCell<HashSet<u64>>,
595}
596
597impl Drop for BindingScopeInner {
598    fn drop(&mut self) {
599        if let Some(inner) = self.graph.upgrade() {
600            inner.scopes.borrow_mut().remove(&self.id);
601            PropertyGraph { inner }.release_owner(self);
602        }
603    }
604}
605
606/// Internal owner-thread attachment lifetime used by mounted nodes.
607#[doc(hidden)]
608#[derive(Clone)]
609pub struct BindingScope {
610    inner: Rc<BindingScopeInner>,
611}
612
613impl BindingScope {
614    /// Evaluate an owner-thread node access without reviving a detached view.
615    pub fn with_graph<R>(&self, body: impl FnOnce() -> R) -> R {
616        let graph = PropertyGraph {
617            inner: self
618                .inner
619                .graph
620                .upgrade()
621                .expect("binding graph was destroyed"),
622        };
623        let _graph = graph.enter();
624        let previous = ACTIVE_SCOPE.with(|active| active.replace(Some(Rc::downgrade(&self.inner))));
625        let _scope = BindingScopeGuard { previous };
626        body()
627    }
628
629    pub fn enter(&self) -> BindingScopeGuard {
630        assert!(
631            !self.inner.closed.get(),
632            "cannot evaluate bindings for an unmounted view"
633        );
634        let previous = ACTIVE_SCOPE.with(|active| active.replace(Some(Rc::downgrade(&self.inner))));
635        BindingScopeGuard { previous }
636    }
637
638    pub fn close(&self) {
639        if self.inner.closed.replace(true) {
640            return;
641        }
642        if let Some(inner) = self.inner.graph.upgrade() {
643            PropertyGraph { inner }.release_owner(&self.inner);
644        }
645    }
646
647    pub fn resume(&self) {
648        if !self.inner.closed.replace(false) {
649            return;
650        }
651        if let Some(inner) = self.inner.graph.upgrade() {
652            let graph = PropertyGraph { inner };
653            let _entered = graph.enter();
654            let _scope = self.enter();
655            self.inner.properties.borrow_mut().retain(|id| {
656                graph.inner.attachments.borrow().contains_key(id)
657                    || graph
658                        .inner
659                        .dormant
660                        .borrow()
661                        .get(id)
662                        .is_some_and(|entry| entry.live())
663            });
664            let properties: Vec<_> = self.inner.properties.borrow().iter().copied().collect();
665            for id in properties {
666                graph.record_owner(id);
667                graph.restore(id);
668            }
669        }
670    }
671}
672
673#[doc(hidden)]
674pub struct BindingScopeGuard {
675    previous: Option<std::rc::Weak<BindingScopeInner>>,
676}
677impl Drop for BindingScopeGuard {
678    fn drop(&mut self) {
679        ACTIVE_SCOPE.with(|active| {
680            active.replace(self.previous.take());
681        });
682    }
683}
684
685/// Restores the previous owner-thread graph when dropped.
686#[must_use]
687pub struct PropertyGraphGuard {
688    previous: Option<PropertyGraph>,
689    previous_scope: Option<std::rc::Weak<BindingScopeInner>>,
690}
691
692impl Drop for PropertyGraphGuard {
693    fn drop(&mut self) {
694        ACTIVE_GRAPH.with(|active| {
695            active.replace(self.previous.take());
696        });
697        ACTIVE_SCOPE.with(|active| {
698            active.replace(self.previous_scope.take());
699        });
700    }
701}