dittolive_ditto/sync/sync_subscription.rs
1use std::{
2 cmp::Ordering,
3 hash::{self, Hash},
4};
5
6use ffi_sdk::{
7 ffi_utils::{c_slice, char_p, repr_c},
8 FfiSyncSubscription,
9};
10
11use crate::{
12 ditto::Ditto,
13 error::DittoError,
14 utils::{extension_traits::FfiResultIntoRustResult, zstr::zstr},
15};
16
17/// Use [`ditto.sync().register_subscription(...)`] to create a `SyncSubscription`
18///
19/// The subscription will remain active until either:
20///
21/// - the `SyncSubscription` is explicitly canceled via [`.cancel()`], or
22/// - the owning [`Ditto`] object goes out of scope
23///
24/// [See the `sync` module documentation for more details][0].
25///
26/// [`ditto.sync().register_subscription(...)`]: crate::sync::Sync::register_subscription
27/// [`.cancel()`]: Self::cancel
28/// [0]: crate::sync
29pub struct SyncSubscription {
30 pub(crate) handle: repr_c::Box<FfiSyncSubscription>,
31}
32
33impl SyncSubscription {
34 pub(crate) fn new(
35 ditto: &Ditto,
36 query: &zstr,
37 query_args: Option<&[u8]>,
38 ) -> Result<Self, DittoError> {
39 let handle = ffi_sdk::dittoffi_sync_register_subscription_throws(
40 &ditto.ditto,
41 query.into(),
42 query_args.map(|a| a.into()),
43 )
44 .into_rust_result()?;
45
46 Ok(Self { handle })
47 }
48
49 /// The DQL query string of this [`SyncSubscription`] (as passed while
50 /// registering it).
51 ///
52 /// This is the original query string supplied when calling
53 /// [`ditto.sync().register_subscription(...)`], not the resolved query with
54 /// arguments substituted in.
55 ///
56 /// # Example
57 ///
58 /// ```
59 /// use dittolive_ditto::prelude::*;
60 ///
61 /// # fn main() -> anyhow::Result<()> {
62 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
63 /// let sync_subscription = ditto.sync().register_subscription("SELECT * FROM cars")?;
64 ///
65 /// assert_eq!(sync_subscription.query_string(), "SELECT * FROM cars");
66 /// # Ok(())
67 /// # }
68 /// ```
69 ///
70 /// [`ditto.sync().register_subscription(...)`]: crate::sync::Sync::register_subscription
71 pub fn query_string(&self) -> String {
72 let cbox: char_p::Box = ffi_sdk::dittoffi_sync_subscription_query_string(&self.handle);
73 cbox.into_string()
74 }
75
76 /// The DQL query arguments of this [`SyncSubscription`] (as passed while
77 /// registering it).
78 ///
79 /// The returned value is not guaranteed to be strictly equal to the value
80 /// provided when calling [`ditto.sync().register_subscription(...)`], as
81 /// the arguments will have gone through a serialization roundtrip. This
82 /// might affect equality checks, particularly for non-primitive values. If
83 /// you want more control over how to deserialize the query arguments then
84 /// use [`query_arguments_cbor_data`](Self::query_arguments_cbor_data) or
85 /// [`query_arguments_json_str`](Self::query_arguments_json_str).
86 ///
87 /// # Example
88 ///
89 /// ```
90 /// use dittolive_ditto::prelude::*;
91 ///
92 /// # fn main() -> anyhow::Result<()> {
93 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
94 /// let sync_subscription = ditto
95 /// .sync()
96 /// .register_subscription((
97 /// "SELECT * FROM cars WHERE color = :color",
98 /// serde_json::json!({
99 /// "color": "red",
100 /// })
101 /// ))?;
102 ///
103 /// let maybe_args = sync_subscription.query_arguments();
104 /// let args = maybe_args.expect("expected query arguments");
105 /// let args_json = serde_json::to_value(&args)?;
106 ///
107 /// assert_eq!(args_json, serde_json::json!({
108 /// "color": "red",
109 /// }));
110 ///
111 /// # Ok(())
112 /// # }
113 /// ```
114 ///
115 /// [`ditto.sync().register_subscription(...)`]: crate::sync::Sync::register_subscription
116 pub fn query_arguments(&self) -> Option<serde_cbor::Value> {
117 let buffer: c_slice::Box<u8> =
118 ffi_sdk::dittoffi_sync_subscription_query_arguments_cbor(&self.handle)?;
119
120 let cbor = serde_cbor::from_slice(buffer.as_slice())
121 .unwrap_or_else(|error| panic!("bug: failed to deserialize CBOR from FFI: {error}"));
122 Some(cbor)
123 }
124
125 /// Returns the DQL query arguments as raw CBOR-encoded bytes.
126 ///
127 /// # Example
128 ///
129 /// ```
130 /// use dittolive_ditto::prelude::*;
131 ///
132 /// # fn main() -> anyhow::Result<()> {
133 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
134 /// let sync_subscription = ditto
135 /// .sync()
136 /// .register_subscription((
137 /// "SELECT * FROM cars WHERE color = :color",
138 /// serde_json::json!({
139 /// "color": "red",
140 /// })
141 /// ))?;
142 ///
143 /// let maybe_args = sync_subscription.query_arguments_cbor_data();
144 /// let args = maybe_args.expect("expected query arguments");
145 /// let args_json: serde_json::Value = serde_cbor::from_slice(&args)?;
146 ///
147 /// assert_eq!(args_json, serde_json::json!({
148 /// "color": "red",
149 /// }));
150 ///
151 /// # Ok(())
152 /// # }
153 /// ```
154 pub fn query_arguments_cbor_data(&self) -> Option<Vec<u8>> {
155 let buffer: c_slice::Box<u8> =
156 ffi_sdk::dittoffi_sync_subscription_query_arguments_cbor(&self.handle)?;
157 Some(buffer.as_slice().to_vec())
158 }
159
160 /// Returns the DQL query arguments as a JSON string.
161 ///
162 /// # Example
163 ///
164 /// ```
165 /// use dittolive_ditto::prelude::*;
166 ///
167 /// # fn main() -> anyhow::Result<()> {
168 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
169 /// let args = serde_json::json!({
170 /// "color": "red",
171 /// });
172 /// let query = (
173 /// "SELECT * FROM cars WHERE color = :color",
174 /// args.clone(),
175 /// );
176 /// let sync_subscription = ditto.sync().register_subscription(query)?;
177 ///
178 /// let maybe_args = sync_subscription.query_arguments_json_str();
179 /// let args_str = maybe_args.expect("expected query arguments");
180 ///
181 /// assert_eq!(args_str, serde_json::to_string(&args).unwrap());
182 ///
183 /// # Ok(())
184 /// # }
185 /// ```
186 pub fn query_arguments_json_str(&self) -> Option<String> {
187 let buffer: c_slice::Box<u8> =
188 ffi_sdk::dittoffi_sync_subscription_query_arguments_json(&self.handle)?;
189
190 let json = String::from_utf8(buffer.as_slice().to_vec())
191 .unwrap_or_else(|error| panic!("bug: failed to deserialize JSON from FFI: {error}"));
192 Some(json)
193 }
194
195 /// Cancels this [`SyncSubscription`], so that changes matching the query are no longer
196 /// synced from other peers to this one.
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// use dittolive_ditto::Ditto;
202 ///
203 /// # fn main() -> anyhow::Result<()> {
204 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
205 /// let subscription = ditto.sync().register_subscription("SELECT * FROM cars")?;
206 /// assert!(!subscription.is_cancelled());
207 ///
208 /// subscription.cancel();
209 /// assert!(subscription.is_cancelled());
210 /// # Ok(())
211 /// # }
212 /// ```
213 pub fn cancel(&self) {
214 ffi_sdk::dittoffi_sync_subscription_cancel(&self.handle);
215 }
216
217 /// Returns `true` if this [`SyncSubscription`] has been cancelled, `false` otherwise.
218 ///
219 /// # Example
220 ///
221 /// ```
222 /// use dittolive_ditto::Ditto;
223 ///
224 /// # fn main() -> anyhow::Result<()> {
225 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
226 /// let subscription = ditto.sync().register_subscription("SELECT * FROM cars")?;
227 /// assert!(!subscription.is_cancelled());
228 ///
229 /// subscription.cancel();
230 /// assert!(subscription.is_cancelled());
231 /// # Ok(())
232 /// # }
233 /// ```
234 pub fn is_cancelled(&self) -> bool {
235 ffi_sdk::dittoffi_sync_subscription_is_cancelled(&self.handle)
236 }
237
238 /// Intentionally left non-public as the ID representation is an implementation detail
239 ///
240 /// The only reason this is here is to power the Ord/Hash/Debug impls
241 fn id(&self) -> impl '_ + Ord + Hash + core::fmt::Debug {
242 ffi_sdk::dittoffi_sync_subscription_id(&self.handle)
243 }
244}
245
246impl std::fmt::Debug for SyncSubscription {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("SyncSubscription")
249 .field("id", &self.id())
250 .finish_non_exhaustive()
251 }
252}
253
254impl std::fmt::Display for SyncSubscription {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 std::fmt::Debug::fmt(self, f)
257 }
258}
259
260impl Ord for SyncSubscription {
261 fn cmp(&self, other: &Self) -> Ordering {
262 Ord::cmp(&self.id(), &other.id())
263 }
264}
265
266impl PartialOrd for SyncSubscription {
267 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
268 Some(Ord::cmp(self, other))
269 }
270}
271
272impl Eq for SyncSubscription {}
273impl PartialEq for SyncSubscription {
274 fn eq(&self, other: &Self) -> bool {
275 self.id() == other.id()
276 }
277}
278
279impl Hash for SyncSubscription {
280 fn hash<H: hash::Hasher>(&self, h: &mut H) {
281 self.id().hash(h)
282 }
283}