dittolive_ditto/store/
observer.rs

1use_prelude!();
2
3use std::hash::{self, Hash};
4
5use ffi_sdk::{
6    ffi_utils::repr_c, FfiDynChangeHandlerWithSignalNext, FfiDynSignalNext, FfiQueryResult,
7    FfiStoreObserver,
8};
9use uuid::Uuid;
10
11pub use super::*;
12use crate::{error::DittoError, utils::zstr::zstr};
13
14/// Use [`ditto.store().register_observer(...)`] to create an observer with a callback.
15///
16/// [`ditto.store().register_observer(...)`]: crate::store::Store::register_observer
17pub struct StoreObserver {
18    pub(crate) handle: repr_c::Box<FfiStoreObserver>,
19}
20
21trait_alias! {
22    /// A change handler is called whenever an active store observer receives new results.
23    pub trait ChangeHandler =
24        FnMut(QueryResult)
25        + Send // can be dropped in another thread
26        + Sync
27        + 'static // cannot dangle
28}
29
30trait_alias! {
31    /// A callback that tells Ditto the observer's handler is ready to receive another callback.
32    ///
33    /// This is a mechanism for handling backpressure when updates arrive faster than the handler
34    /// can process them. Ditto pauses further callbacks, but continues to apply changes to the
35    /// local store. If multiple relevant changes occur while paused, Ditto does not queue a
36    /// callback for each change. After this callback is invoked, Ditto calls the handler once with
37    /// the latest result if that result differs from the last result delivered.
38    pub trait SignalNext =
39        FnOnce()
40        + 'static // cannot dangle
41        + Send // can be dropped in another thread
42}
43
44trait_alias! {
45    /// A change handler is called whenever an active store observer receives new results.
46    ///
47    /// Call the provided [`SignalNext`] function to signal that the handler is ready to
48    /// receive the next callback from the store observer. See [`SignalNext`] for how this
49    /// mechanism handles backpressure and collapses pending updates into at most one callback
50    /// while delivery is paused.
51    pub trait ChangeHandlerWithSignalNext =
52        FnMut(QueryResult, Box<dyn SignalNext>)
53        + 'static // cannot dangle
54        + Send // can be dropped in another thread
55        + Sync
56}
57
58fn dittoffi_store_observer_register_safe<F>(
59    ditto: &'_ Ditto,
60    query: &zstr,
61    query_args: Option<&[u8]>,
62    mut observer: F,
63) -> Result<StoreObserver>
64where
65    F: ChangeHandlerWithSignalNext,
66{
67    let ffi_callback: repr_c::Box<FfiDynChangeHandlerWithSignalNext> = Box::new(
68        move |ffi_query_result: repr_c::Box<FfiQueryResult>,
69              signal_next: repr_c::Arc<FfiDynSignalNext>,
70              _: Blocking| {
71            let signal_next = Box::new(move || {
72                #[allow(deprecated)]
73                signal_next.call();
74            });
75
76            observer(QueryResult::from(ffi_query_result), signal_next);
77        },
78    )
79    .into();
80
81    let query = query.into();
82    let query_args_cbor = query_args.map(|qa| qa.into());
83
84    let handle = ffi_sdk::dittoffi_store_register_observer_throws(
85        &ditto.ditto,
86        query,
87        query_args_cbor,
88        ffi_callback,
89    )
90    .into_rust_result()?;
91
92    Ok(StoreObserver { handle })
93}
94
95impl StoreObserver {
96    pub(crate) fn new<F>(
97        ditto: &Ditto,
98        query: &zstr,
99        query_args: Option<&[u8]>,
100        mut on_change: F,
101    ) -> Result<Self, DittoError>
102    where
103        F: ChangeHandler,
104    {
105        Self::with_signal_next(ditto, query, query_args, move |args, signal_next| {
106            on_change(args);
107            signal_next();
108        })
109    }
110
111    pub(crate) fn with_signal_next<F>(
112        ditto: &Ditto,
113        query: &zstr,
114        query_args: Option<&[u8]>,
115        on_change: F,
116    ) -> Result<Self, DittoError>
117    where
118        F: ChangeHandlerWithSignalNext,
119    {
120        let on_change = {
121            let on_change = ::std::sync::Mutex::new(on_change);
122            move |arg: QueryResult, signal_next: Box<dyn SignalNext>| {
123                let mut on_change = on_change
124                    .lock()
125                    .expect("`on_change` observer not to be poisoned");
126                on_change(arg, signal_next)
127            }
128        };
129        dittoffi_store_observer_register_safe(ditto, query, query_args, on_change)
130    }
131
132    /// Return the query string used to create this `StoreObserver`.
133    pub fn query_string(&self) -> String {
134        let char_p = ffi_sdk::dittoffi_store_observer_query_string(&self.handle);
135        char_p.into_string()
136    }
137
138    /// Return the query arguments used to create this `StoreObserver`, if any.
139    pub fn query_arguments(&self) -> Option<serde_cbor::Value> {
140        let buffer: c_slice::Box<u8> =
141            ffi_sdk::dittoffi_store_observer_query_arguments(&self.handle)?;
142        let cbor = serde_cbor::from_slice(buffer.as_slice())
143            .unwrap_or_else(|error| panic!("bug: failed to deserialize CBOR from FFI: {error}"));
144
145        Some(cbor)
146    }
147
148    /// Cancels this [`StoreObserver`] and its callback.
149    ///
150    /// The callback registered with this [`StoreObserver`] will no longer be called
151    /// after calling [`.cancel()`].
152    ///
153    /// This call is a no-op if this [`StoreObserver`] has already been cancelled
154    /// or if the owning [`Ditto`] object has gone out of scope.
155    ///
156    /// [`.cancel()`]: Self::cancel
157    pub fn cancel(&self) {
158        ffi_sdk::dittoffi_store_observer_cancel(&self.handle);
159    }
160
161    /// Returns true if this [`StoreObserver`] has been cancelled.
162    pub fn is_cancelled(&self) -> bool {
163        ffi_sdk::dittoffi_store_observer_is_cancelled(&self.handle)
164    }
165
166    /// Returns the unique identifier for this [`StoreObserver`].
167    fn id(&self) -> Uuid {
168        let buffer = ffi_sdk::dittoffi_store_observer_id(&self.handle);
169        Uuid::from_slice(buffer.as_slice()).expect("bug: expected valid UUID")
170    }
171}
172
173impl StoreObserver {
174    fn comparable_parts(&self) -> impl '_ + Eq + Hash {
175        self.id()
176    }
177}
178
179impl Eq for StoreObserver {}
180impl PartialEq for StoreObserver {
181    fn eq(&self, other: &Self) -> bool {
182        self.comparable_parts() == other.comparable_parts()
183    }
184}
185
186impl Hash for StoreObserver {
187    fn hash<H: hash::Hasher>(&self, h: &mut H) {
188        self.comparable_parts().hash(h)
189    }
190}