dittolive_ditto/ditto/init/
mod.rs

1use std::{
2    ffi::CString,
3    future::Future,
4    os::raw::{c_uint, c_void},
5    str::FromStr,
6    sync::{Arc, Weak},
7    time::Duration,
8};
9
10use async_fn_traits::AsyncFn2;
11use async_trait::async_trait;
12use extern_c::extern_c;
13use ffi_sdk::{FsComponent, TransportConfigMode};
14
15pub use self::config::{DittoConfig, DittoConfigConnect};
16use crate::{
17    auth::DittoAuthenticator,
18    ditto::{
19        init::config::{ActualConfig, InternalConfig},
20        DittoFields,
21    },
22    error,
23    small_peer_info::SmallPeerInfo,
24    utils::{make_continuation, prelude::*},
25    warn,
26};
27
28pub(crate) mod config;
29
30impl Ditto {
31    /// Open a new Ditto instance using a [`DittoConfig`]
32    ///
33    /// # Example
34    ///
35    /// ```
36    /// # use dittolive_ditto::prelude::*;
37    /// # async fn example() -> anyhow::Result<()> {
38    /// // Load your database ID somehow, ENV is a good option
39    /// let database_id = std::env::var("DITTO_DATABASE_ID")?;
40    ///
41    /// // Choose one of the following types of connection config
42    /// let connect = DittoConfigConnect::Server {
43    ///     url: "https://example.com/your-server-url".parse().unwrap(),
44    /// };
45    /// let connect = DittoConfigConnect::SmallPeersOnly {
46    ///     private_key: Some("https://example.com/your-server-url".bytes().collect()),
47    /// };
48    /// let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
49    ///
50    /// let config = DittoConfig::new(database_id, connect);
51    /// let ditto = Ditto::open(config).await?;
52    /// # Ok(())
53    /// # }
54    /// ```
55    pub async fn open(config: DittoConfig) -> Result<Ditto, DittoError> {
56        config
57            .validate_system_parameter_value_shapes()
58            .map_err(|message| DittoError::from_str(ErrorKind::InvalidInput, message))?;
59        Self::init_sdk_version();
60        let default_root_dir = default_root_directory();
61
62        let customer_config = config;
63        let mut internal_config = InternalConfig {
64            legacy_persistence_directory: None,
65        };
66
67        // SDKS-3187: When the user hasn't set an explicit persistence directory,
68        // provide the v4 default path as a legacy fallback so that v4→v5 upgrades
69        // don't silently create a new empty database. The v4 Rust SDK used the
70        // executable's parent directory directly as the default root.
71        if customer_config.persistence_directory.is_none() {
72            if let Some(root) = &default_root_dir {
73                internal_config.legacy_persistence_directory = Some(root.clone());
74            }
75        }
76
77        let actual_config = ActualConfig {
78            customer_facing: customer_config,
79            internal: internal_config,
80        };
81
82        let config_cbor: &[u8] =
83            &serde_cbor::to_vec(&actual_config).expect("should serialize well-formed DittoConfig");
84        let (continuation, recv) = make_continuation();
85        let default_root_dir_ref = default_root_dir
86            .as_deref()
87            .and_then(|root| root.to_str())
88            .ok_or_else(|| {
89                DittoError::new(
90                    ErrorKind::IO,
91                    "Unable to resolve a default data directory on this platform".to_string(),
92                )
93            })?;
94        let default_root_dir_cstring = CString::from_str(default_root_dir_ref)
95            .expect("should construct CString from no-nulls &str");
96        let default_root_dir_cstr = &*default_root_dir_cstring;
97        let default_root_dir_charp: char_p::Ref<'_> = default_root_dir_cstr.into();
98
99        // CLIPPY: continuation.into() needed to pass CI
100        #[allow(clippy::useless_conversion)]
101        ffi_sdk::dittoffi_ditto_open_async_throws(
102            config_cbor.into(),
103            TransportConfigMode::PlatformIndependent,
104            default_root_dir_charp,
105            continuation.into(),
106        );
107
108        let ffi_result = recv.await.unwrap();
109        let ffi_ditto: repr_c::Box<ffi_sdk::Ditto> = ffi_result.into_rust_result()?;
110
111        Self::finish_open(ffi_ditto, actual_config.customer_facing)
112    }
113
114    /// Open a new Ditto instance using a [`DittoConfig`]
115    ///
116    /// This is a synchronous blocking variant of [`Ditto::open()`] that will wait until
117    /// initialization is complete.
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// # use dittolive_ditto::prelude::*;
123    /// # fn example() -> anyhow::Result<()> {
124    /// // Load your database ID somehow, ENV is a good option
125    /// let database_id = std::env::var("DITTO_DATABASE_ID")?;
126    ///
127    /// // Choose one of the following types of connection config
128    /// let connect = DittoConfigConnect::Server {
129    ///     url: "https://example.com/your-server-url".parse().unwrap(),
130    /// };
131    /// let connect = DittoConfigConnect::SmallPeersOnly {
132    ///     private_key: Some("https://example.com/your-server-url".bytes().collect()),
133    /// };
134    /// let connect = DittoConfigConnect::SmallPeersOnly { private_key: None };
135    ///
136    /// let config = DittoConfig::new(database_id, connect);
137    /// let ditto = Ditto::open_sync(config)?;
138    /// # Ok(())
139    /// # }
140    /// ```
141    pub fn open_sync(config: DittoConfig) -> Result<Ditto, DittoError> {
142        config
143            .validate_system_parameter_value_shapes()
144            .map_err(|message| DittoError::from_str(ErrorKind::InvalidInput, message))?;
145        Self::init_sdk_version();
146        let default_root_dir = default_root_directory();
147
148        let customer_config = config;
149        let mut internal_config = InternalConfig {
150            legacy_persistence_directory: None,
151        };
152
153        // SDKS-3187: Same legacy fallback as open() above.
154        if customer_config.persistence_directory.is_none() {
155            if let Some(root) = &default_root_dir {
156                internal_config.legacy_persistence_directory = Some(root.clone());
157            }
158        }
159
160        let actual_config = ActualConfig {
161            customer_facing: customer_config,
162            internal: internal_config,
163        };
164
165        let config_cbor: &[u8] =
166            &serde_cbor::to_vec(&actual_config).expect("should serialize well-formed DittoConfig");
167        let default_root_dir_ref = default_root_dir
168            .as_deref()
169            .and_then(|root| root.to_str())
170            .ok_or_else(|| {
171                DittoError::new(
172                    ErrorKind::IO,
173                    "Unable to resolve a default data directory on this platform".to_string(),
174                )
175            })?;
176        let default_root_dir_cstring = CString::from_str(default_root_dir_ref)
177            .expect("should construct CString from no-nulls &str");
178        let default_root_dir_cstr = &*default_root_dir_cstring;
179        let default_root_dir_charp: char_p::Ref<'_> = default_root_dir_cstr.into();
180
181        let ffi_ditto: repr_c::Box<ffi_sdk::Ditto> = ffi_sdk::dittoffi_ditto_open_throws(
182            config_cbor.into(),
183            TransportConfigMode::PlatformIndependent,
184            default_root_dir_charp,
185        )
186        .into_rust_result()?;
187
188        Self::finish_open(ffi_ditto, actual_config.customer_facing)
189    }
190
191    fn finish_open(
192        ffi_ditto: repr_c::Box<ffi_sdk::Ditto>,
193        config: DittoConfig,
194    ) -> Result<Ditto, DittoError> {
195        let ditto: Arc<repr_c::Box<ffi_sdk::Ditto>> = Arc::new(ffi_ditto);
196        let has_auth = matches!(&config.connect, DittoConfigConnect::Server { .. });
197
198        let disk_usage = DiskUsage::new(ditto.retain(), FsComponent::Root);
199        let small_peer_info = SmallPeerInfo::new(ditto.retain());
200        let fields = Arc::new_cyclic(|weak_fields: &arc::Weak<_>| {
201            let store = Store::new(ditto.retain(), weak_fields.clone());
202            let sync = crate::sync::Sync::new(weak_fields.clone());
203            let presence = Arc::new(Presence::new(weak_fields.clone()));
204
205            DittoFields {
206                ditto: ditto.retain(),
207                has_auth,
208                config,
209                store,
210                sync,
211                presence,
212                disk_usage,
213                small_peer_info,
214            }
215        });
216
217        let ditto = Ditto {
218            fields,
219            is_shut_down_able: true,
220        };
221
222        Ok(ditto)
223    }
224}
225
226fn default_root_directory() -> Option<PathBuf> {
227    std::env::current_exe()
228        .ok()
229        .and_then(|abspath| abspath.parent().map(|x| x.to_path_buf()))
230}
231
232impl DittoAuthenticator {
233    /// Set a callback to notify the client when the authentication is expiring.
234    ///
235    /// When using `DittoConfigConnect::Server { .. }` mode, Ditto _requires_ you to register an
236    /// "expiration handler" for authentication. This handler is called for an initial
237    /// authentication and periodically thereafter when the current authentication is near to
238    /// expiration.
239    ///
240    /// For more details about authentication, see the [Ditto Auth and Authorization docs][0].
241    ///
242    /// [0]: https://docs.ditto.live/sdk/latest/auth-and-authorization/cloud-authentication#login
243    ///
244    /// For more details about the expiration handler, see the [`DittoAuthExpirationHandler`] trait.
245    ///
246    /// # Example
247    ///
248    /// ```
249    /// # #[allow(deprecated)]
250    /// # use tracing::error;
251    /// # use dittolive_ditto::prelude::*;
252    /// # fn main() -> anyhow::Result<()> {
253    /// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_online_ditto();
254    /// async fn sample_get_token() -> anyhow::Result<String> {
255    ///     // e.g. reqwest::get("https://example.com/token").await?.text().await?;
256    ///     Ok("token".to_string())
257    /// }
258    ///
259    /// let auth = ditto
260    ///     .auth()
261    ///     .expect("Auth is available in Server connect mode");
262    /// auth.set_expiration_handler(async |ditto: &Ditto, duration_remaining| {
263    ///     let auth = ditto
264    ///         .auth()
265    ///         .expect("Auth is available in Server connect mode");
266    ///
267    ///     // Call your auth service to get a new token
268    ///     let token = sample_get_token().await.unwrap();
269    ///
270    ///     // Where "my-provider" is the name of the Authentication Webhook
271    ///     // you've configured on the Ditto Portal
272    ///     let result = auth.login(&token, "my-provider");
273    ///
274    ///     if let Err(login_error) = result {
275    ///         // Handle login error, e.g.:
276    ///         error!("Failed to login: {}", login_error);
277    ///     }
278    /// });
279    /// # Ok(())
280    /// # }
281    /// ```
282    pub fn set_expiration_handler<F>(&self, handler: F)
283    where
284        F: DittoAuthExpirationHandler,
285    {
286        let Some(ditto) = self.ditto_fields.upgrade() else {
287            #[allow(deprecated)] // Workaround for patched tracing
288            {
289                error!("Failed to set expiration handler, Ditto has shut down");
290            }
291            return;
292        };
293
294        let login_provider = make_login_provider(self.ditto_fields.clone(), Arc::new(handler));
295        ffi_sdk::ditto_auth_set_login_provider(&ditto.ditto, Some(login_provider));
296    }
297
298    /// Clear the expiration handler, if set.
299    pub fn clear_expiration_handler(&self) {
300        let Some(ditto) = self.ditto_fields.upgrade() else {
301            #[allow(deprecated)] // Workaround for patched tracing
302            {
303                error!("Failed to clear expiration handler, Ditto has shut down");
304            }
305            return;
306        };
307
308        ffi_sdk::ditto_auth_set_login_provider(&ditto.ditto, None);
309    }
310}
311
312/// Trait describing types which can be used as an authentication expiration handler for Ditto.
313///
314/// When using `DittoConfigConnect::Server { .. }` mode, Ditto _requires_ you to register an
315/// "expiration handler" for authentication. This handler is called for an initial authentication
316/// and periodically thereafter when the current authentication is near expiration.
317///
318/// This trait is implemented for async closures and for functions returning an
319/// `impl Future<Output = ()>`, which take the expected arguments of an auth expiration handler.
320///
321/// Expiration handlers are expected to call `auth.login(...)` with a valid authentication token.
322///
323/// For more details about authentication, see the [Ditto Auth and Authorization docs][0].
324///
325/// [0]: https://docs.ditto.live/sdk/latest/auth-and-authorization/cloud-authentication#login
326///
327/// **NOTE**: Because expiration handlers are asynchronous, they must not block the thread.
328///
329/// # Example
330///
331/// ```
332/// # use std::time::Duration;
333/// # use dittolive_ditto::prelude::*;
334/// # let (_root, ditto) = dittolive_ditto::doctest_helpers::doctest_online_ditto();
335/// let auth = ditto
336///     .auth()
337///     .expect("Auth is available for DittoConfigConnect::Server mode");
338///
339/// // Option 1: Use an async closure
340/// auth.set_expiration_handler(async |ditto: &Ditto, duration| {
341///     // Your authentication handler code here
342/// });
343///
344/// // Option 2: Use a closure returning an async block
345/// auth.set_expiration_handler(|ditto: &Ditto, duration| async {
346///     // Your authentication handler code here
347/// });
348///
349/// // Option 3: Use a custom type and trait impl
350/// struct MyAuthHandler;
351/// impl DittoAuthExpirationHandler for MyAuthHandler {
352///     async fn on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
353///         // Your authentication handler code here
354///     }
355/// }
356/// auth.set_expiration_handler(MyAuthHandler);
357/// ```
358pub trait DittoAuthExpirationHandler: 'static + Send + Sync {
359    /// Provide an async handler that will be called when the authentication is expiring.
360    fn on_expiration(
361        &self,
362        ditto: &Ditto,
363        duration_remaining: Duration,
364    ) -> impl Send + Future<Output = ()>;
365}
366
367impl<F> DittoAuthExpirationHandler for F
368where
369    F: 'static + Send + Sync,
370    F: for<'r> AsyncFn2<&'r Ditto, Duration, Output = (), OutputFuture: Send>,
371{
372    async fn on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
373        self(ditto, duration_remaining).await
374    }
375}
376
377#[async_trait]
378pub(crate) trait DynDittoAuthExpirationHandler: 'static + Send + Sync {
379    async fn dyn_on_expiration(&self, ditto: &Ditto, duration_remaining: Duration);
380}
381
382#[async_trait]
383impl<F: DittoAuthExpirationHandler> DynDittoAuthExpirationHandler for F {
384    async fn dyn_on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
385        self.on_expiration(ditto, duration_remaining).await
386    }
387}
388
389impl DittoAuthExpirationHandler for dyn '_ + DynDittoAuthExpirationHandler {
390    async fn on_expiration(&self, ditto: &Ditto, duration_remaining: Duration) {
391        self.dyn_on_expiration(ditto, duration_remaining).await
392    }
393}
394
395pub(crate) fn make_login_provider(
396    ditto_fields: Weak<DittoFields>,
397    auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
398) -> repr_c::Box<ffi_sdk::LoginProvider> {
399    struct LoginProviderCtx {
400        ditto_fields: Weak<DittoFields>,
401        auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
402    }
403
404    let login_provider_ctx = Arc::new(LoginProviderCtx {
405        auth_expiration_handler,
406        ditto_fields,
407    });
408
409    let ffi_ctx = Arc::as_ptr(&login_provider_ctx) as *mut c_void;
410    let ffi_retain = Some(extern_c(|ctx: *mut c_void| unsafe {
411        Arc::<LoginProviderCtx>::increment_strong_count(ctx.cast())
412    }) as unsafe extern "C" fn(_));
413    let ffi_release = Some(extern_c(|ctx: *mut c_void| unsafe {
414        Arc::<LoginProviderCtx>::decrement_strong_count(ctx.cast())
415    }) as unsafe extern "C" fn(_));
416
417    // This callback is just a "trigger" to initiate the caller's authentication handler
418    // The handler is async and spawned in a separate task
419    let ffi_handler = extern_c(|ctx: *mut c_void, secs_remaining: c_uint| {
420        let login_provider_ctx: &LoginProviderCtx = unsafe { &*ctx.cast() };
421        let auth_expiration_handler = login_provider_ctx.auth_expiration_handler.retain();
422        let Ok(ditto) = Ditto::upgrade(&login_provider_ctx.ditto_fields) else {
423            #[allow(deprecated)] // Workaround for patched tracing
424            {
425                error!("Failed to dispatch auth handler, Ditto has been shut down");
426            }
427            return;
428        };
429
430        dispatch_auth_handler(
431            auth_expiration_handler,
432            ditto,
433            Duration::from_secs(secs_remaining.into()),
434        );
435    });
436
437    unsafe {
438        ffi_sdk::ditto_auth_client_make_login_provider(
439            ffi_ctx,
440            ffi_retain,
441            ffi_release,
442            ffi_handler,
443        )
444    }
445}
446
447/// Dispatches the users's authentication expiration handler.
448///
449/// This function checks whether the current execution context is within a tokio runtime.
450///
451/// - If a tokio runtime is available, the handler is spawned onto a task.
452/// - If no tokio runtime is available, a temporary current-thread runtime is created and the
453///   dispatcher will block until the handler completes.
454///
455/// # Panics
456///
457/// This function will panic if:
458///
459/// - There is no current tokio runtime available, and
460/// - We fail to create a temporary current-thread runtime
461fn dispatch_auth_handler(
462    auth_expiration_handler: Arc<dyn DynDittoAuthExpirationHandler>,
463    ditto: Ditto,
464    duration_remaining: Duration,
465) {
466    // Check if we're in a tokio runtime context
467    match tokio::runtime::Handle::try_current() {
468        Ok(handle) => {
469            // We have a tokio runtime, spawn the handler as usual
470            handle.spawn(async move {
471                auth_expiration_handler
472                    .on_expiration(&ditto, duration_remaining)
473                    .await;
474            });
475        }
476        Err(_) => {
477            // No tokio runtime available, create a temporary current-thread runtime
478            #[allow(deprecated)] // Workaround for patched tracing
479            {
480                warn!(
481                    "No tokio runtime available for expiration handler. Creating temporary \
482                     runtime."
483                );
484            }
485
486            match tokio::runtime::Builder::new_current_thread()
487                .enable_all()
488                .build()
489            {
490                Ok(rt) => {
491                    rt.block_on(async move {
492                        auth_expiration_handler
493                            .on_expiration(&ditto, duration_remaining)
494                            .await;
495                    });
496                }
497                Err(e) => {
498                    panic!(
499                        "Failed to create tokio runtime for expiration handler: {}. Consider \
500                         running within a tokio runtime context.",
501                        e
502                    );
503                }
504            }
505        }
506    }
507}
508
509#[cfg(test)]
510mod tests {
511
512    #[test]
513    fn test_runtime_detection_behavior() {
514        // This test demonstrates the runtime detection behavior
515        println!("Testing runtime detection...");
516
517        // Check if we're in a tokio context (should be false in regular test)
518        match tokio::runtime::Handle::try_current() {
519            Ok(_handle) => {
520                println!("✓ Tokio runtime detected - handlers will be spawned");
521            }
522            Err(_) => {
523                println!("✗ No tokio runtime - will create temporary runtime");
524
525                // Demonstrate temporary runtime creation (as done in make_login_provider)
526                match tokio::runtime::Builder::new_current_thread()
527                    .enable_all()
528                    .build()
529                {
530                    Ok(_rt) => {
531                        println!("✓ Successfully created temporary runtime");
532                    }
533                    Err(e) => {
534                        println!("✗ Failed to create temporary runtime: {}", e);
535                    }
536                }
537            }
538        }
539    }
540
541    #[tokio::test]
542    async fn test_with_tokio_runtime_available() {
543        // This test runs with a tokio runtime available
544        println!("Testing with tokio runtime available...");
545
546        // This should succeed
547        match tokio::runtime::Handle::try_current() {
548            Ok(_handle) => {
549                println!("✓ Tokio runtime is available for async handlers");
550            }
551            Err(_) => {
552                panic!("Expected tokio runtime to be available in tokio::test");
553            }
554        }
555
556        println!("✓ Runtime detection works correctly in async context");
557    }
558
559    #[test]
560    fn test_improved_error_handling() {
561        // Test that we can create the same type of runtime as the implementation
562        let rt_result = tokio::runtime::Builder::new_current_thread()
563            .enable_all()
564            .build();
565
566        match rt_result {
567            Ok(rt) => {
568                println!("✓ Temporary runtime creation works");
569
570                // Demonstrate that we can use it for async work
571                rt.block_on(async {
572                    println!("✓ Async work executes successfully in temporary runtime");
573                });
574            }
575            Err(e) => {
576                println!("✗ Failed to create runtime: {}", e);
577            }
578        }
579    }
580}