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;