Skip to main content

tessera_ui/runtime/
state_owner.rs

1//! Reactive state with an explicit controller or route lifetime.
2//!
3//! ## Usage
4//!
5//! Create a state owner and share its keyed state through component props.
6
7use std::{
8    any::TypeId,
9    hash::{Hash, Hasher},
10    sync::{
11        Arc,
12        atomic::{AtomicBool, Ordering},
13    },
14};
15
16use rustc_hash::FxHasher;
17
18use super::{SlotKey, State, resolve_state_key, with_slot_table_mut};
19
20struct OwnerLease {
21    id: u128,
22    session: u128,
23    closed: AtomicBool,
24    dispatcher: crate::UiDispatcher,
25}
26
27impl OwnerLease {
28    fn close(&self) {
29        if self.closed.swap(true, Ordering::AcqRel) {
30            return;
31        }
32        let owner = self.id;
33        if crate::session::current_session_identity() == Some(self.session) {
34            release_slots(owner);
35        } else {
36            self.dispatcher.dispatch(move || release_slots(owner));
37        }
38    }
39}
40
41impl Drop for OwnerLease {
42    fn drop(&mut self) {
43        self.close();
44    }
45}
46
47/// Owns reactive state for a controller, route, or other explicit UI lifetime.
48///
49/// Clones share the lifetime. Closing the owner, or dropping its final clone,
50/// releases its state. Cleanup requested from another thread runs on the owning
51/// UI session before its next frame.
52#[derive(Clone)]
53pub struct StateOwner(Arc<OwnerLease>);
54
55impl Default for StateOwner {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl PartialEq for StateOwner {
62    fn eq(&self, other: &Self) -> bool {
63        Arc::ptr_eq(&self.0, &other.0)
64    }
65}
66
67impl Eq for StateOwner {}
68
69impl StateOwner {
70    /// Creates an owner in the current UI session.
71    pub fn new() -> Self {
72        let owner = Arc::new(OwnerLease {
73            id: uuid::Uuid::new_v4().as_u128(),
74            session: crate::session::current_session_identity()
75                .expect("state owners require an active UI session"),
76            closed: AtomicBool::new(false),
77            dispatcher: crate::dispatcher::current(),
78        });
79        Self(owner)
80    }
81
82    /// Returns whether this lifetime still accepts state.
83    pub fn is_alive(&self) -> bool {
84        !self.0.closed.load(Ordering::Acquire) && self.0.dispatcher.is_alive()
85    }
86
87    /// Closes the lifetime even when other clones still exist.
88    pub fn close(&self) {
89        self.0.close();
90    }
91
92    /// Captures result delivery that stops when this owner closes.
93    pub fn dispatcher(&self) -> crate::UiDispatcher {
94        let owner = Arc::downgrade(&self.0);
95        self.0.dispatcher.clone().scoped(move || {
96            owner
97                .upgrade()
98                .is_some_and(|owner| !owner.closed.load(Ordering::Acquire))
99        })
100    }
101
102    /// Gets or initializes reactive state by a key within this lifetime.
103    ///
104    /// Reads and writes use the same build and layout invalidation mechanisms
105    /// as remembered state. The owning UI thread performs initialization.
106    pub fn state_with_key<K, T>(&self, key: K, init: impl FnOnce() -> T) -> State<T>
107    where
108        K: Hash,
109        T: Send + Sync + 'static,
110    {
111        assert_eq!(
112            crate::session::current_session_identity(),
113            Some(self.0.session),
114            "state owner belongs to a different UI session"
115        );
116        assert!(self.is_alive(), "state owner is closed");
117        let mut hasher = FxHasher::default();
118        key.hash(&mut hasher);
119        let key = SlotKey {
120            owned_owner: Some(self.0.id),
121            instance_logic_id: 0,
122            slot_hash: hasher.finish(),
123            type_id: TypeId::of::<T>(),
124        };
125        resolve_state_key(key, true, || {
126            let value = init();
127            assert!(self.is_alive(), "state owner closed during initialization");
128            value
129        })
130    }
131}
132
133fn release_slots(owner: u128) {
134    let removed = with_slot_table_mut(|table| {
135        let slots = table.owned_slots.remove(&owner).unwrap_or_default();
136        slots
137            .into_iter()
138            .filter_map(|slot| table.remove_slot(slot))
139            .collect::<Vec<_>>()
140    });
141    drop(removed);
142}