Skip to main content

dioxus_fullstack/
lazy.rs

1#![allow(clippy::needless_return)]
2
3use dioxus_core::CapturedError;
4use std::{hint::black_box, prelude::rust_2024::Future, sync::atomic::AtomicBool};
5
6/// `Lazy` is a thread-safe, lazily-initialized global variable.
7///
8/// Unlike other async once-cell implementations, accessing the value of a `Lazy` instance is synchronous
9/// and done on `deref`.
10///
11/// This is done by offloading the async initialization to a blocking thread during the first access,
12/// and then using the initialized value for all subsequent accesses.
13///
14/// It uses `std::sync::OnceLock` internally to ensure that the value is only initialized once.
15pub struct Lazy<T> {
16    value: std::sync::OnceLock<T>,
17    started_initialization: AtomicBool,
18    constructor: Option<fn() -> Result<T, CapturedError>>,
19    _phantom: std::marker::PhantomData<T>,
20}
21
22impl<T: Send + Sync + 'static> Lazy<T> {
23    /// Create a new `Lazy` instance.
24    ///
25    /// This internally calls `std::sync::OnceLock::new()` under the hood.
26    #[allow(clippy::self_named_constructors)]
27    pub const fn lazy() -> Self {
28        Self {
29            _phantom: std::marker::PhantomData,
30            constructor: None,
31            started_initialization: AtomicBool::new(false),
32            value: std::sync::OnceLock::new(),
33        }
34    }
35
36    pub const fn new<F, G, E>(constructor: F) -> Self
37    where
38        F: Fn() -> G + Copy,
39        G: Future<Output = Result<T, E>> + Send + 'static,
40        E: Into<CapturedError>,
41    {
42        if std::mem::size_of::<F>() != 0 {
43            panic!(
44                "The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables."
45            );
46        }
47
48        // Prevent the constructor from being optimized out
49        black_box(constructor);
50
51        Self {
52            _phantom: std::marker::PhantomData,
53            value: std::sync::OnceLock::new(),
54            started_initialization: AtomicBool::new(false),
55            constructor: Some(blocking_initialize::<T, F, G, E>),
56        }
57    }
58
59    /// Set the value of the `Lazy` instance.
60    ///
61    /// This should only be called once during the server setup phase, typically inside `dioxus::serve`.
62    /// Future calls to this method will return an error containing the provided value.
63    pub fn set(&self, pool: T) -> Result<(), CapturedError> {
64        let res = self.value.set(pool);
65        if res.is_err() {
66            return Err(anyhow::anyhow!("Lazy value is already initialized.").into());
67        }
68
69        Ok(())
70    }
71
72    pub fn try_set(&self, pool: T) -> Result<(), T> {
73        self.value.set(pool)
74    }
75
76    /// Initialize the value of the `Lazy` instance if it hasn't been initialized yet.
77    pub fn initialize(&self) -> Result<(), CapturedError> {
78        if let Some(constructor) = self.constructor {
79            // If we're already initializing this value, wait on the receiver.
80            if self
81                .started_initialization
82                .swap(true, std::sync::atomic::Ordering::SeqCst)
83            {
84                self.value.wait();
85                return Ok(());
86            }
87
88            // Otherwise, we need to initialize the value
89            self.set(constructor().unwrap())?;
90        }
91        Ok(())
92    }
93
94    /// Get a reference to the value of the `Lazy` instance. This will block the current thread if the
95    /// value is not yet initialized.
96    pub fn get(&self) -> &T {
97        if self.constructor.is_none() {
98            return self.value.get().expect("Lazy value is not initialized. Make sure to call `initialize` before dereferencing.");
99        };
100
101        if self.value.get().is_none() {
102            self.initialize().expect("Failed to initialize lazy value");
103        }
104
105        self.value.get().unwrap()
106    }
107}
108
109impl<T: Send + Sync + 'static> Default for Lazy<T> {
110    fn default() -> Self {
111        Self::lazy()
112    }
113}
114
115impl<T: Send + Sync + 'static> std::ops::Deref for Lazy<T> {
116    type Target = T;
117
118    fn deref(&self) -> &Self::Target {
119        self.get()
120    }
121}
122
123impl<T: std::fmt::Debug + Send + Sync + 'static> std::fmt::Debug for Lazy<T> {
124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        f.debug_struct("Lazy").field("value", self.get()).finish()
126    }
127}
128
129/// This is a small hack that allows us to staple the async initialization into a blocking context.
130///
131/// We call the `rust-call` method of the zero-sized constructor function. This is safe because we're
132/// not actually dereferencing any unsafe data, just calling its vtable entry to get the future.
133fn blocking_initialize<T, F, G, E>() -> Result<T, CapturedError>
134where
135    T: Send + Sync + 'static,
136    F: Fn() -> G + Copy,
137    G: Future<Output = Result<T, E>> + Send + 'static,
138    E: Into<CapturedError>,
139{
140    assert_eq!(
141        std::mem::size_of::<F>(),
142        0,
143        "The constructor function must be a zero-sized type (ZST). Consider using a function pointer or a closure without captured variables."
144    );
145
146    #[cfg(feature = "server")]
147    {
148        let ptr: F = unsafe { std::mem::zeroed() };
149        let fut = ptr();
150        return std::thread::spawn(move || {
151            tokio::runtime::Builder::new_current_thread()
152                .enable_all()
153                .build()
154                .unwrap()
155                .block_on(fut)
156                .map_err(|e| e.into())
157        })
158        .join()
159        .unwrap();
160    }
161
162    // todo: technically we can support constructors in wasm with the same tricks inventory uses with `__wasm_call_ctors`
163    // the host would need to decide when to cal the ctors and when to block them.
164    #[cfg(not(feature = "server"))]
165    unimplemented!("Lazy initialization is only supported with tokio and threads enabled.")
166}