dittolive_ditto/ditto/mod.rs
1//! # Entry point for the DittoSDK
2//!
3//! `Ditto` is a cross-platform peer-to-peer database that allows apps to sync with and even without
4//! internet connectivity.
5//!
6//! To manage your local data and connections to other peers in the mesh, `Ditto` gives you access
7//! to:
8//! * [`Store`], the entry point to the database.
9//! * [`TransportConfig`] to change the transport layers in use.
10//! * [`Presence`] to monitor peers in the mesh.
11//! * [`DiskUsage`] to monitor local Ditto disk usage.
12
13use_prelude!();
14
15pub mod init;
16
17use std::{
18 env,
19 sync::{Once, Weak},
20};
21
22/// The log levels that the Ditto SDK supports.
23pub use ffi_sdk::CLogLevel as LogLevel;
24use ffi_sdk::{BoxedDitto, Platform};
25use uuid::Uuid;
26
27use crate::{
28 disk_usage::DiskUsage,
29 ditto::init::{config::ActualConfig, DittoConfig, DittoConfigConnect},
30 error::{DittoError, ErrorKind, LicenseTokenError},
31 identity::DittoAuthenticator,
32 presence::Presence,
33 small_peer_info::SmallPeerInfo,
34 transport::TransportConfig,
35 utils::{extension_traits::FfiResultIntoRustResult, prelude::*},
36};
37
38static SDK_VERSION_INIT: Once = Once::new();
39
40#[extension(pub(crate) trait TryUpgrade)]
41impl std::sync::Weak<BoxedDitto> {
42 fn try_upgrade(&self) -> Result<Arc<BoxedDitto>, ErrorKind> {
43 self.upgrade().ok_or(ErrorKind::ReleasedDittoInstance)
44 }
45}
46
47/// The entrypoint for accessing all Ditto functionality.
48///
49/// Use the `Ditto` object to access all other Ditto APIs, such as:
50///
51/// - [`ditto.store()`] to access the [`Store`] API and read and write data on this peer
52/// - [`ditto.sync()`] to access the [`Sync`] API and sync data with other peers
53/// - [`ditto.presence()`] to access the [`Presence`] API and inspect connected peers
54/// - [`ditto.small_peer_info()`] to access the [`SmallPeerInfo`] API and manage peer metadata
55/// - [`ditto.disk_usage()`] to access the [`DiskUsage`] API and inspect disk usage
56///
57/// [`ditto.store()`]: crate::Ditto::store
58/// [`ditto.sync()`]: crate::Ditto::sync
59/// [`ditto.presence()`]: crate::Ditto::presence
60/// [`ditto.small_peer_info()`]: crate::Ditto::small_peer_info
61/// [`ditto.disk_usage()`]: crate::Ditto::disk_usage
62pub struct Ditto {
63 pub(crate) fields: Arc<DittoFields>,
64 /// Always `true` except in `Auth::logout()`.
65 is_shut_down_able: bool,
66}
67
68impl std::ops::Deref for Ditto {
69 type Target = DittoFields;
70
71 #[inline]
72 fn deref(&'_ self) -> &'_ DittoFields {
73 &self.fields
74 }
75}
76
77impl Ditto {
78 pub(crate) fn upgrade(weak: &Weak<DittoFields>) -> Result<Ditto> {
79 let fields = weak.upgrade().ok_or(ErrorKind::ReleasedDittoInstance)?;
80 Ok(Ditto::new_temp(fields))
81 }
82}
83
84/// Inner fields for Ditto
85#[doc(hidden)]
86// TODO(pub_check)
87pub struct DittoFields {
88 // FIXME: (Ham & Daniel) - ideally we'd use this in the same way as we do
89 // with the `fields` on `Ditto` (only ever extracting weak references)
90 pub(crate) ditto: Arc<ffi_sdk::BoxedDitto>,
91 has_auth: bool,
92 config: DittoConfig,
93 pub(crate) store: Store,
94 pub(crate) sync: crate::sync::Sync,
95 presence: Arc<Presence>,
96 disk_usage: DiskUsage,
97 small_peer_info: SmallPeerInfo,
98}
99
100// We use this pattern to ensure `self` is not used after `ManuallyDrop::take()`-ing its fields.
101impl Drop for Ditto {
102 fn drop(&mut self) {
103 if self.is_shut_down_able {
104 // stop all transports
105 self.sync().stop();
106 // Here, ditto is implicitly dropped using ditto_free if there is no strong reference to
107 // it anymore. We need to make sure that `ditto_shutdown` is called before
108 // `ditto_free` gets called though, as this will perform all the necessary
109 // pre-drop actions such as stopping TCP servers, etc.
110 ffi_sdk::ditto_shutdown(&self.ditto);
111 }
112 }
113}
114
115// Public interface for modifying Transport configuration.
116impl Ditto {
117 /// Clean shutdown of the Ditto instance
118 pub fn close(self) {
119 // take ownership of Ditto in order to drop it
120 }
121
122 /// Set a new [`TransportConfig`] and begin syncing over these
123 /// transports. Any change to start or stop a specific transport should proceed via providing a
124 /// modified configuration to this method.
125 pub fn set_transport_config(&self, config: TransportConfig) {
126 let cbor = serde_cbor::to_vec(&config).expect("bug: failed to serialize TransportConfig");
127 ffi_sdk::dittoffi_ditto_try_set_transport_config(&self.ditto, (&*cbor).into(), false)
128 .into_rust_result()
129 .expect("bug: core failed to set transport config");
130 }
131
132 /// Convenience method to update the current transport config of the receiver.
133 ///
134 /// Invokes the block with a copy of the current transport config which
135 /// you can alter to your liking. The updated transport config is then set
136 /// on the receiver.
137 ///
138 /// You may use this method to alter the configuration at any time.
139 /// Sync will not begin until [`ditto.sync().start()`] is invoked.
140 ///
141 /// # Example
142 ///
143 /// Edit the config by simply mutating the `&mut TransportConfig` passed
144 /// to your callback:
145 ///
146 /// ```
147 /// use dittolive_ditto::prelude::*;
148 /// # fn main() -> anyhow::Result<()> {
149 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
150 ///
151 /// // Enable the TCP listener on port 4000
152 /// ditto.update_transport_config(|config| {
153 /// config.listen.tcp.enabled = true;
154 /// config.listen.tcp.interface_ip = "0.0.0.0".to_string();
155 /// config.listen.tcp.port = 4000;
156 /// });
157 /// # Ok(())
158 /// # }
159 /// ```
160 ///
161 /// [`ditto.sync().start()`]: crate::sync::Sync::start
162 pub fn update_transport_config(&self, update: impl FnOnce(&mut TransportConfig)) {
163 let mut transport_config = self.transport_config();
164 update(&mut transport_config);
165 self.set_transport_config(transport_config);
166 }
167
168 /// Returns a snapshot of the currently configured transports.
169 ///
170 /// # Example
171 ///
172 /// ```
173 /// # use dittolive_ditto::prelude::*;
174 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
175 /// let transport_config = ditto.transport_config();
176 /// println!("Current transport config: {transport_config:#?}");
177 /// ```
178 pub fn transport_config(&self) -> TransportConfig {
179 let transport_config_cbor = ffi_sdk::dittoffi_ditto_transport_config(&self.ditto);
180 serde_cbor::from_slice::<TransportConfig>(transport_config_cbor.as_slice())
181 .expect("bug: failed to deserialize TransportConfig from core")
182 }
183}
184
185impl Ditto {
186 /// Activate an offline [`Ditto`] instance by setting a license token.
187 ///
188 /// You cannot initiate sync on an offline
189 /// ([`DittoConfigConnect::SmallPeersOnly`])
190 /// [`Ditto`] instance before you have activated it.
191 pub fn set_offline_only_license_token(&self, license_token: &str) -> Result<(), DittoError> {
192 if matches!(
193 self.config.connect,
194 DittoConfigConnect::SmallPeersOnly { .. }
195 ) {
196 use ffi_sdk::LicenseVerificationResult;
197 use safer_ffi::prelude::{AsOut, ManuallyDropMut};
198 let c_license: char_p::Box = char_p::new(license_token);
199
200 let mut err_msg = None;
201 let out_err_msg = err_msg.manually_drop_mut().as_out();
202 let res =
203 ffi_sdk::ditto_verify_license(&self.ditto, c_license.as_ref(), Some(out_err_msg));
204
205 if res == LicenseVerificationResult::LicenseOk {
206 return Ok(());
207 }
208
209 let err_msg = err_msg.unwrap();
210 #[allow(deprecated)] // Workaround for patched tracing
211 {
212 error!("{err_msg}");
213 }
214
215 match res {
216 LicenseVerificationResult::LicenseExpired => {
217 Err(DittoError::license(LicenseTokenError::Expired {
218 message: err_msg.as_ref().to_string(),
219 }))
220 }
221 LicenseVerificationResult::VerificationFailed => {
222 Err(DittoError::license(LicenseTokenError::VerificationFailed {
223 message: err_msg.as_ref().to_string(),
224 }))
225 }
226 LicenseVerificationResult::UnsupportedFutureVersion => Err(DittoError::license(
227 LicenseTokenError::UnsupportedFutureVersion {
228 message: err_msg.as_ref().to_string(),
229 },
230 )),
231 _ => panic!("Unexpected license verification result {:?}", res),
232 }
233 } else {
234 Err(DittoError::new(
235 ErrorKind::Internal,
236 "Offline license tokens should only be used for SharedKey or OfflinePlayground \
237 identities",
238 ))
239 }
240 }
241
242 /// Look for a license token from a given environment variable.
243 pub fn set_license_from_env(&self, var_name: &str) -> Result<(), DittoError> {
244 match env::var(var_name) {
245 Ok(token) => self.set_offline_only_license_token(&token),
246 Err(env::VarError::NotPresent) => {
247 let msg = format!("No license token found for env var {}", &var_name);
248 Err(DittoError::from_str(ErrorKind::Config, msg))
249 }
250 Err(e) => Err(DittoError::new(ErrorKind::Config, e)),
251 }
252 }
253}
254
255impl Ditto {
256 /// Returns a reference to the underlying local data store.
257 pub fn store(&self) -> &Store {
258 &self.store
259 }
260
261 /// Entrypoint to Ditto's [`Sync`] API for syncing documents between peers.
262 ///
263 /// [`Sync`]: crate::sync::Sync
264 pub fn sync(&self) -> &crate::sync::Sync {
265 &self.sync
266 }
267
268 #[cfg(feature = "preview-datastreams")]
269 /// Returns a handle to the Data Streams Endpoint. This can be used to open streams to remote
270 /// peers.
271 ///
272 /// NOTE: This API is in preview and may change in future releases.
273 pub fn datastreams(&self) -> crate::preview::datastreams::Endpoint {
274 ffi_sdk::dittoffi_get_preview_datastreams(&self.ditto)
275 }
276
277 /// Return a reference to the [`SmallPeerInfo`] object.
278 pub fn small_peer_info(&self) -> &SmallPeerInfo {
279 &self.small_peer_info
280 }
281
282 /// The absolute path to the persistence directory used by Ditto to persist data.
283 ///
284 /// This returns the final, resolved absolute file path where Ditto stores its data.
285 /// The value depends on what was provided in [`DittoConfig::persistence_directory`].
286 ///
287 /// - If an **absolute path** was provided, it returns that path unchanged.
288 /// - If a **relative path** was provided, it returns the path resolved relative to the default
289 /// root directory.
290 /// - If no path was provided, it returns the default path using the pattern
291 /// `{default_root}/ditto-{database-id}` where `{default_root}` corresponds to the default
292 /// root directory and `{database-id}` is the Ditto database ID in lowercase.
293 ///
294 /// This property always returns a consistent value throughout the lifetime of the Ditto
295 /// instance and represents the actual directory being used for persistence.
296 ///
297 /// - Note: "Database ID" was previously referred to as "App ID" in older versions of the SDK.
298 ///
299 /// - Note: It is not recommended to directly read or write to this directory as its structure
300 /// and contents are managed by Ditto and may change in future versions.
301 ///
302 /// - Note: When [`DittoLogger`] is enabled, logs may be written to this directory even after a
303 /// Ditto instance has been deallocated. Please refer to the documentation of [`DittoLogger`]
304 /// for more information.
305 ///
306 /// - See also: [`DittoConfig::persistence_directory`]
307 ///
308 /// [`DittoConfig::persistence_directory`]: DittoConfig::persistence_directory
309 pub fn absolute_persistence_directory(&self) -> PathBuf {
310 let path = ffi_sdk::dittoffi_ditto_absolute_persistence_directory(&self.ditto);
311 PathBuf::from(path.to_str())
312 }
313
314 /// Returns an owned snapshot of the _effective_ `DittoConfig` as used by the core library.
315 ///
316 /// - Modifying this `DittoConfig` has no effect on the active Ditto configuration.
317 /// - The returned `DittoConfig` may be different than the one passed to [`Ditto::open`] or
318 /// [`Ditto::open_sync`] because it will have resolved details such as the absolute path to
319 /// the persistence directory.
320 pub fn config(&self) -> DittoConfig {
321 let config_cbor = ffi_sdk::dittoffi_ditto_config(&self.ditto);
322 serde_cbor::from_slice::<ActualConfig>(&config_cbor)
323 .expect("bug: should deserialize DittoConfig")
324 .customer_facing
325 }
326
327 /// Set a custom identifier for the current device.
328 ///
329 /// When using [`presence`](Ditto::presence), each remote peer is represented by a
330 /// short UTF-8 "device name". By default this will be a truncated version of the device's
331 /// hostname. It does not need to be unique among peers. Configure the device name before
332 /// calling [`ditto.sync().start()`](crate::sync::Sync::start). If it is too long it will be
333 /// truncated.
334 pub fn set_device_name(&self, name: &str) {
335 let c_device_name: char_p::Box = char_p::new(name.to_owned());
336 // We don't currently expose the device name to the user so we don't
337 // need to worry about the returned (potentially truncated) value here
338 let _ = ffi_sdk::ditto_set_device_name(&self.ditto, c_device_name.as_ref());
339 }
340
341 /// Return a handle to the [`Presence`] API to monitor peers' activity in the Ditto mesh.
342 ///
343 /// # Example
344 ///
345 /// Use [`ditto.presence().graph()`] to request a current [`PresenceGraph`] of connected peers:
346 ///
347 /// ```
348 /// use dittolive_ditto::prelude::*;
349 ///
350 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
351 /// let presence_graph: PresenceGraph = ditto.presence().graph();
352 /// println!("Ditto mesh right now: {presence_graph:#?}");
353 /// ```
354 ///
355 /// # Example
356 ///
357 /// Use [`ditto.presence().register_observer(...)`] to subscribe to changes in mesh presence:
358 ///
359 /// ```
360 /// use dittolive_ditto::prelude::*;
361 ///
362 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
363 /// let _observer = ditto.presence().register_observer(|graph| {
364 /// println!("Ditto mesh update! {graph:#?}");
365 /// });
366 ///
367 /// // The observer is cancelled when dropped.
368 /// // In a real application, hold onto it for as long as you need it alive.
369 /// drop(_observer);
370 /// ```
371 ///
372 /// [`ditto.presence().graph()`]: crate::presence::Presence::graph
373 /// [`PresenceGraph`]: crate::presence::PresenceGraph
374 /// [`ditto.presence().register_observer(...)`]: crate::presence::Presence::register_observer
375 pub fn presence(&self) -> &Arc<Presence> {
376 &self.presence
377 }
378
379 /// Return a [`DiskUsage`] to monitor the disk usage of the Ditto
380 /// instance. It can be used to retrieve an immediate representation of the Ditto file system:
381 ///
382 /// ```
383 /// # use dittolive_ditto::prelude::Ditto;
384 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
385 /// let fs_tree = ditto.disk_usage().item();
386 /// ```
387 /// Or to bind a callback to the changes:
388 /// ```
389 /// # use dittolive_ditto::prelude::Ditto;
390 /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_ditto();
391 /// let handle = ditto.disk_usage().observe(|fs_tree| {
392 /// // do something with the graph
393 /// });
394 /// // The handle must be kept to keep receiving updates on the file system.
395 /// // To stop receiving update, drop the handle.
396 /// ```
397 pub fn disk_usage(&self) -> &DiskUsage {
398 &self.disk_usage
399 }
400
401 /// Returns the current [`DittoAuthenticator`], if it exists.
402 ///
403 /// The [`DittoAuthenticator`] is available when using [`DittoConfigConnect::Server`] mode.
404 pub fn auth(&self) -> Option<DittoAuthenticator> {
405 self.fields.has_auth.then(|| DittoAuthenticator {
406 ditto_fields: Arc::downgrade(&self.fields),
407 })
408 }
409
410 /// Returns `true` if this `Ditto` instance has been activated with a valid
411 /// license token.
412 pub fn is_activated(&self) -> bool {
413 ffi_sdk::dittoffi_ditto_is_activated(&self.ditto)
414 }
415 fn platform() -> Platform {
416 using!(match () {
417 use ffi_sdk::Platform;
418 | _case if cfg!(target_os = "windows") => Platform::Windows,
419 | _case if cfg!(target_os = "android") => Platform::Android,
420 | _case if cfg!(target_os = "macos") => Platform::Mac,
421 | _case if cfg!(target_os = "ios") => Platform::Ios,
422 | _case if cfg!(target_os = "tvos") => Platform::Tvos,
423 | _case if cfg!(target_os = "linux") => Platform::Linux,
424 | _default => Platform::Unknown,
425 })
426 }
427 fn sdk_version() -> String {
428 let sdk_semver = env!("CARGO_PKG_VERSION");
429 sdk_semver.to_string()
430 }
431
432 fn init_sdk_version() {
433 SDK_VERSION_INIT.call_once(|| {
434 let platform = Self::platform();
435 let sdk_semver = Self::sdk_version();
436 let c_version = char_p::new(sdk_semver);
437 ffi_sdk::ditto_init_sdk_version(platform, ffi_sdk::Language::Rust, c_version.as_ref());
438 });
439 }
440 /// Return the version of the SDK.
441 pub fn version() -> String {
442 Self::init_sdk_version();
443 ffi_sdk::dittoffi_get_sdk_semver().to_string()
444 }
445}
446
447// Constructors
448impl Ditto {
449 // This isn't public to customers and is only used internally. It's only currently used when we
450 // have access to the `DittoFields` and want to create a `Ditto` instance for whatever reason,
451 // but we don't want `Ditto` to be shut down when this `Ditto` instance is dropped.
452 //
453 // This should definitely be considered a hack for now.
454 pub(crate) fn new_temp(fields: Arc<DittoFields>) -> Ditto {
455 Ditto {
456 fields,
457 is_shut_down_able: false,
458 }
459 }
460}
461
462impl Ditto {
463 /// Removes all sync metadata for any remote peers which aren't currently connected. This method
464 /// shouldn't usually be called. Manually running garbage collection often will result in slower
465 /// sync times. Ditto automatically runs a garbage a collection process in the background at
466 /// optimal times.
467 ///
468 /// Manually running garbage collection is typically only useful during testing if large amounts
469 /// of data are being generated. Alternatively, if an entire data set is to be evicted and it's
470 /// clear that maintaining this metadata isn't necessary, then garbage collection could be run
471 /// after evicting the old data.
472 pub fn run_garbage_collection(&self) {
473 ffi_sdk::ditto_run_garbage_collection(&self.ditto);
474 }
475}
476
477#[derive(Clone, Debug)]
478// pub struct DatabaseId(uuid::Uuid); // Demo apps still use arbitrary strings
479/// The ID of this Ditto database, used to determine which peers to sync with
480pub struct DatabaseId(pub(crate) String); // neither String nor Vec<u8> are Copy
481
482impl DatabaseId {
483 /// Generate a random DatabaseId from a UUIDv4
484 pub fn generate() -> Self {
485 let uuid = uuid::Uuid::new_v4();
486 DatabaseId::from_uuid(uuid)
487 }
488
489 /// Generate a DatabaseId from a given UUIDv4
490 pub fn from_uuid(uuid: Uuid) -> Self {
491 let id_str = format!("{:x}", &uuid); // lower-case with hypens
492 DatabaseId(id_str)
493 }
494
495 /// Attempt to grab a specific DatabaseId from some environment variable
496 pub fn from_env(var: &str) -> Result<Self, DittoError> {
497 let id_str = env::var(var).map_err(|err| DittoError::new(ErrorKind::Config, err))?;
498 Ok(DatabaseId(id_str))
499 }
500
501 /// Return the corresponding string
502 pub fn as_str(&self) -> &str {
503 &self.0
504 }
505
506 /// Return the corresponding c string
507 pub fn to_c_string(&self) -> char_p::Box {
508 char_p::new(self.0.as_str())
509 }
510
511 /// Return the default auth URL associated with the database ID. This is of the form
512 /// `https://{database_id}.cloud.ditto.live/` by default.
513 pub fn default_auth_url(&self) -> String {
514 format!("https://{}.cloud.ditto.live", self.0)
515 }
516
517 /// Return the default WebSocket sync URL which is of the form
518 /// `wss://{database_id}.cloud.ditto.live/` by default.
519 pub fn default_sync_url(&self) -> String {
520 format!("wss://{}.cloud.ditto.live", self.0)
521 }
522}
523
524use std::{fmt, fmt::Display, str::FromStr};
525
526impl Display for DatabaseId {
527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
528 write!(f, "{}", self.0)
529 }
530}
531
532impl FromStr for DatabaseId {
533 type Err = DittoError;
534 fn from_str(s: &str) -> Result<DatabaseId, DittoError> {
535 // later s will need to be a valid UUIDv4
536 Ok(DatabaseId(s.to_string()))
537 }
538}
539
540#[cfg(test)]
541#[path = "tests.rs"]
542mod tests;