Skip to main content

spacedb_crdt/
reactive.rs

1//! Reactive queries — re-evaluate as the document changes.
2//!
3//! A [`CrdtDoc`] bumps a revision counter on every change (local mutation *or*
4//! applied remote update). [`Watcher`] observes that counter; [`ReactiveQuery`]
5//! turns it into a live query that re-evaluates when the document changes and
6//! **emits a result only when the result actually changes** — the "re-render as
7//! the mesh converges" behaviour the SDK exposes as a signal/stream.
8//!
9//! Polling, not callbacks: the yrs update observer fires *inside* the mutating
10//! transaction, where opening another transaction (to read fields) would panic.
11//! So changes are recorded as a counter bump and the query re-evaluates safely
12//! afterwards, when the caller polls.
13
14use std::cell::Cell;
15use std::sync::atomic::{AtomicU64, Ordering};
16use std::sync::Arc;
17
18use crate::CrdtDoc;
19
20/// Watches a document's revision counter for changes.
21pub struct Watcher {
22    revision: Arc<AtomicU64>,
23    last_seen: Cell<u64>,
24}
25
26impl Watcher {
27    pub(crate) fn new(revision: Arc<AtomicU64>) -> Self {
28        let current = revision.load(Ordering::Relaxed);
29        Self {
30            revision,
31            last_seen: Cell::new(current),
32        }
33    }
34
35    /// Return whether the document has changed since the last call, consuming the
36    /// pending change so a subsequent call returns `false` until the next change.
37    pub fn drain_changed(&self) -> bool {
38        let current = self.revision.load(Ordering::Relaxed);
39        if current != self.last_seen.get() {
40            self.last_seen.set(current);
41            true
42        } else {
43            false
44        }
45    }
46
47    /// The current revision of the watched document.
48    pub fn revision(&self) -> u64 {
49        self.revision.load(Ordering::Relaxed)
50    }
51}
52
53/// A live query over a document: re-evaluates `query` when the document changes
54/// and yields the new result only when it differs from the last emitted one.
55pub struct ReactiveQuery<R, F> {
56    watcher: Watcher,
57    query: F,
58    last: R,
59}
60
61impl<R, F> ReactiveQuery<R, F>
62where
63    R: Clone + PartialEq,
64    F: Fn(&CrdtDoc) -> R,
65{
66    /// Build a reactive query over `doc`, evaluating `query` once for the initial
67    /// value.
68    pub fn new(doc: &CrdtDoc, query: F) -> Self {
69        let watcher = doc.watch();
70        let last = query(doc);
71        Self {
72            watcher,
73            query,
74            last,
75        }
76    }
77
78    /// Poll for an update. Returns `Some(new_result)` if the document changed
79    /// *and* the query result changed since the last emission (the pushed delta);
80    /// otherwise `None`.
81    pub fn poll(&mut self, doc: &CrdtDoc) -> Option<R> {
82        if !self.watcher.drain_changed() {
83            return None;
84        }
85        let next = (self.query)(doc);
86        if next != self.last {
87            self.last = next.clone();
88            Some(next)
89        } else {
90            None
91        }
92    }
93
94    /// The last evaluated result.
95    pub fn current(&self) -> &R {
96        &self.last
97    }
98}