Skip to main content

pax_runtime_api/properties/
shared_property.rs

1use super::{LocalProperty, PropertyBinding, PropertyValue, UntypedProperty};
2use crate::Interpolatable;
3use serde::{Deserialize, Serialize};
4use std::{
5    cell::Cell,
6    collections::HashMap,
7    sync::{
8        atomic::{AtomicU64, Ordering},
9        Arc, Mutex, MutexGuard, Weak,
10    },
11};
12
13use super::shared_graph::{with_current_graph, GraphInbox};
14
15/// Values that can be published by a shared [`Property`].
16///
17/// Local computations and values containing `Rc` belong in [`LocalProperty`].
18pub trait SharedPropertyValue: PropertyValue + Send + Sync {}
19impl<T: PropertyValue + Send + Sync> SharedPropertyValue for T {}
20
21static NEXT_ID: AtomicU64 = AtomicU64::new(1);
22
23pub(super) fn next_id() -> u64 {
24    NEXT_ID
25        .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
26        .expect("property identity space exhausted")
27}
28
29pub(super) fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
30    // Writers build a private draft before committing. A panicking update
31    // leaves the published snapshot intact, so poisoning does not lose data.
32    mutex
33        .lock()
34        .unwrap_or_else(|poisoned| poisoned.into_inner())
35}
36
37thread_local! {
38    static MUTATING: Cell<bool> = const { Cell::new(false) };
39}
40
41struct MutationGuard;
42
43impl MutationGuard {
44    fn enter() -> Self {
45        MUTATING.with(|active| {
46            assert!(
47                !active.replace(true),
48                "property mutation callbacks must not mutate another property recursively"
49            );
50        });
51        Self
52    }
53}
54
55impl Drop for MutationGuard {
56    fn drop(&mut self) {
57        MUTATING.with(|active| active.set(false));
58    }
59}
60
61pub(super) struct Snapshot<T> {
62    pub revision: u64,
63    pub value: Arc<T>,
64}
65
66pub(super) struct SharedCell<T> {
67    pub id: u64,
68    value: Mutex<Snapshot<T>>,
69    writer: Mutex<()>,
70    subscribers: Mutex<HashMap<u64, Weak<GraphInbox>>>,
71}
72
73impl<T> Drop for SharedCell<T> {
74    fn drop(&mut self) {
75        // The last shared owner can disappear on a worker. Wake the owner to
76        // release its local binding; never destroy an evaluator on this thread.
77        let subscribers = self
78            .subscribers
79            .get_mut()
80            .unwrap_or_else(|e| e.into_inner());
81        for inbox in subscribers.values().filter_map(Weak::upgrade) {
82            inbox.publish(self.id);
83        }
84    }
85}
86
87impl<T: SharedPropertyValue> SharedCell<T> {
88    pub fn snapshot(&self) -> Snapshot<T> {
89        let snapshot = lock(&self.value);
90        Snapshot {
91            revision: snapshot.revision,
92            value: snapshot.value.clone(),
93        }
94    }
95
96    fn change(&self, origin: Option<u64>, update: impl FnOnce(&T) -> Option<T>) -> (bool, u64) {
97        let mutation = MutationGuard::enter();
98        let writer = lock(&self.writer);
99        let before = self.snapshot();
100        let Some(value) = update(&before.value) else {
101            return (false, before.revision);
102        };
103        let revision = before
104            .revision
105            .checked_add(1)
106            .expect("property revision exhausted");
107        let value = Arc::new(value);
108        let previous = {
109            let mut current = lock(&self.value);
110            std::mem::replace(&mut *current, Snapshot { revision, value })
111        };
112        drop(writer);
113        drop(mutation);
114        // Value destructors and wake callbacks are arbitrary code. They must
115        // run after all publication locks and the mutation guard are released.
116        drop(previous);
117        drop(before);
118        self.notify(origin);
119        (true, revision)
120    }
121
122    fn notify(&self, origin: Option<u64>) {
123        let subscribers: Vec<_> = {
124            let mut subscribers = lock(&self.subscribers);
125            subscribers.retain(|_, inbox| inbox.strong_count() != 0);
126            subscribers
127                .iter()
128                .filter(|(id, _)| Some(**id) != origin)
129                .filter_map(|(_, inbox)| inbox.upgrade())
130                .collect()
131        };
132        for inbox in subscribers {
133            inbox.publish(self.id);
134        }
135    }
136
137    pub fn publish_local(&self, value: T, graph: u64) -> u64 {
138        self.change(Some(graph), |_| Some(value)).1
139    }
140
141    pub fn subscribe(&self, id: u64, inbox: &Arc<GraphInbox>) {
142        lock(&self.subscribers).insert(id, Arc::downgrade(inbox));
143    }
144
145    pub fn unsubscribe(&self, id: u64) {
146        lock(&self.subscribers).remove(&id);
147    }
148}
149
150/// Thread-safe application state with snapshot reads and atomic publication.
151///
152/// Clones share the value, including when moved into a worker or Tokio task.
153/// Reading never evaluates a UI expression. Use [`Self::local`] inside an
154/// entered [`super::PropertyGraph`] to connect to owner-thread computations.
155/// A write publishes immediately; attached graphs observe it on their next
156/// import. Intermediate revisions may coalesce. Use a channel for events that
157/// must each be delivered.
158pub struct Property<T> {
159    pub(super) cell: Arc<SharedCell<T>>,
160}
161
162impl<T> Clone for Property<T> {
163    fn clone(&self) -> Self {
164        Self {
165            cell: self.cell.clone(),
166        }
167    }
168}
169
170impl<T: SharedPropertyValue> Property<T> {
171    /// Creates independent shared state. No UI thread or graph is required.
172    pub fn new(value: T) -> Self {
173        Self {
174            cell: Arc::new(SharedCell {
175                id: next_id(),
176                value: Mutex::new(Snapshot {
177                    revision: 0,
178                    value: Arc::new(value),
179                }),
180                writer: Mutex::new(()),
181                subscribers: Mutex::new(HashMap::new()),
182            }),
183        }
184    }
185
186    /// Export read-only shared snapshots, including owner-thread computed output.
187    pub fn published(&self) -> Published<T> {
188        Published(self.clone())
189    }
190
191    /// Returns a clone of the latest published value on any thread.
192    pub fn get(&self) -> T {
193        self.read(Clone::clone)
194    }
195
196    /// Reads an immutable snapshot without holding a storage lock during `f`.
197    /// Reentrant reads and writes are permitted here.
198    pub fn read<R>(&self, f: impl FnOnce(&T) -> R) -> R {
199        let snapshot = self.cell.snapshot();
200        f(&snapshot.value)
201    }
202
203    /// Revision imported or published by this thread's entered UI graph.
204    /// Returns None if this property has no active attachment in that graph.
205    /// This diagnostic never imports data or evaluates a computation.
206    pub fn observed_revision(&self) -> Option<u64> {
207        with_current_graph(|graph| graph.observed_revision(self))
208    }
209
210    /// Atomically publishes a value and requests work from attached graphs.
211    pub fn set(&self, value: T) {
212        self.cell.change(None, |_| Some(value));
213    }
214
215    pub(crate) fn publish_guarded(
216        &self,
217        value: T,
218        authority: &crate::async_runtime::Authority,
219    ) -> Result<(), crate::async_runtime::Closed<T>> {
220        let mutation = MutationGuard::enter();
221        let Some(authority) = authority.acquire() else {
222            return Err(crate::async_runtime::Closed(value));
223        };
224        let writer = lock(&self.cell.writer);
225        let previous = {
226            let mut current = lock(&self.cell.value);
227            let revision = current
228                .revision
229                .checked_add(1)
230                .expect("property revision exhausted");
231            std::mem::replace(
232                &mut *current,
233                Snapshot {
234                    revision,
235                    value: Arc::new(value),
236                },
237            )
238        };
239        drop(writer);
240        drop(authority);
241        drop(mutation);
242        // Revocation and commit have finished before arbitrary drop/wake code.
243        drop(previous);
244        self.cell.notify(None);
245        Ok(())
246    }
247
248    /// Updates one property atomically, invoking `f` exactly once.
249    ///
250    /// The closure edits a private draft. A panic leaves the published value
251    /// unchanged. Neither this closure nor value cloning/equality invoked
252    /// during mutation may recursively mutate a property; this is diagnosed
253    /// with a panic before acquiring another write lock. Snapshot reads are
254    /// allowed. Keep callbacks short and never wait for a worker or UI task.
255    pub fn update(&self, f: impl FnOnce(&mut T)) {
256        self.cell.change(None, |old| {
257            let mut draft = old.clone();
258            f(&mut draft);
259            Some(draft)
260        });
261    }
262
263    /// Compares and publishes under the same single-property write operation.
264    pub fn set_if_neq(&self, value: T) -> bool
265    where
266        T: PartialEq,
267    {
268        self.cell
269            .change(None, |old| (old != &value).then_some(value))
270            .0
271    }
272
273    /// Monotonic revision of the currently published value.
274    pub fn revision(&self) -> u64 {
275        self.cell.snapshot().revision
276    }
277
278    /// Obtains this graph's local view of the shared value.
279    ///
280    /// Requires an entered [`super::PropertyGraph`]. The view participates in
281    /// lazy computation and two-way binding on its owner thread. Worker writes
282    /// become visible to the view when that graph imports publications.
283    pub fn local(&self) -> LocalProperty<T> {
284        with_current_graph(|graph| graph.attach(self))
285    }
286
287    /// Returns a local dependency handle. This handle cannot cross threads.
288    pub fn untyped(&self) -> UntypedProperty {
289        self.local().untyped()
290    }
291
292    /// Installs an owner-thread binding while retaining this shared identity.
293    pub fn replace_with(&self, source: LocalProperty<T>) {
294        let local = self.local();
295        local.replace_with(source);
296    }
297}
298
299impl<T: SharedPropertyValue> Default for Property<T> {
300    fn default() -> Self {
301        Self::new(T::default())
302    }
303}
304
305impl<T: SharedPropertyValue + std::fmt::Debug> std::fmt::Debug for Property<T> {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        self.read(|value| f.debug_tuple("Property").field(value).finish())
308    }
309}
310
311impl<T: SharedPropertyValue> Interpolatable for Property<T> {
312    fn interpolate(&self, other: &Self, t: f64) -> Self {
313        Self::new(self.get().interpolate(&other.get(), t))
314    }
315}
316
317impl<T: SharedPropertyValue + Serialize> Serialize for Property<T> {
318    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
319        self.read(|value| value.serialize(serializer))
320    }
321}
322
323impl<'de, T: SharedPropertyValue + Deserialize<'de>> Deserialize<'de> for Property<T> {
324    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325        T::deserialize(deserializer).map(Self::new)
326    }
327}
328
329impl<T: SharedPropertyValue> PropertyBinding<T> for Property<T> {
330    fn from_value(value: T) -> Self {
331        Self::new(value)
332    }
333    fn local(&self) -> LocalProperty<T> {
334        self.local()
335    }
336    fn from_local(source: LocalProperty<T>) -> Self {
337        with_current_graph(|graph| graph.share(source))
338    }
339    fn value_for_conversion(&self) -> T {
340        if super::GRAPH_CONVERSION.with(Cell::get) {
341            self.local().get()
342        } else {
343            self.get()
344        }
345    }
346}
347
348/// A transferable read-only view of published state. Reading never evaluates a
349/// UI binding. Its local computation is independent: rebinding or mutating that
350/// local result cannot publish into the original shared cell.
351///
352/// ```compile_fail
353/// let published = pax_runtime_api::Property::new(1u32).published();
354/// published.set(2);
355/// ```
356#[derive(Clone)]
357pub struct Published<T>(Property<T>);
358impl<T: SharedPropertyValue> Published<T> {
359    pub fn get(&self) -> T {
360        self.0.get()
361    }
362    pub fn read<R>(&self, callback: impl FnOnce(&T) -> R) -> R {
363        self.0.read(callback)
364    }
365    pub fn revision(&self) -> u64 {
366        self.0.revision()
367    }
368    pub fn local(&self) -> LocalProperty<T> {
369        let source = self.0.local();
370        let deps = [source.untyped()];
371        LocalProperty::computed(move || source.get(), &deps)
372    }
373}