Skip to main content

leptos_leaflet/core/
thread_safe_jsvalue.rs

1/// This takes inspiration from the `SendWrapper` crate.
2/// But is specialized for JsValue based objects. Includes support for some extra traits
3/// and a way to convert values into ThreadSafeJsValue.
4///
5/// There is also a macro to implement the From trait for ThreadSafeJsValue, that adds a type alias
6/// with a suffix.
7
8use std::{
9    mem::{self, ManuallyDrop},
10    ops::Deref,
11};
12
13use paste::paste;
14use wasm_bindgen::{convert::IntoWasmAbi, JsValue};
15
16const NOT_ON_CURRENT_THREAD: &str = "ThreadSafeJsValue is not on the current thread";
17
18/// A wrapper around a value that is intended to be passed between threads.
19///
20/// The main use is to wrap JsValue based objects that are not Send or Sync.
21/// There is a small overhead to check if the value is on the current thread.
22pub struct ThreadSafeJsValue<T> {
23    value: ManuallyDrop<T>,
24    thread_id: std::thread::ThreadId,
25}
26
27impl<T> ThreadSafeJsValue<T> {
28    /// Creates a new ThreadSafeJsValue.
29    ///
30    /// # Example
31    ///
32    /// ```
33    /// use wasm_bindgen::JsValue;
34    /// use thread_safe_jsvalue::ThreadSafeJsValue;
35    ///
36    /// let value = 42;
37    ///
38    /// let value_ts = ThreadSafeJsValue::new(value);
39    ///
40    /// assert_eq!(value_ts.value(), &42);
41    /// ```
42    pub fn new(value: T) -> Self {
43        Self {
44            value: ManuallyDrop::new(value),
45            thread_id: std::thread::current().id(),
46        }
47    }
48}
49
50impl<T> Drop for ThreadSafeJsValue<T> {
51    /// Drops the value if it is on the current thread.
52    ///
53    /// # Panics
54    ///
55    /// Panics if the value is not on the current thread, except when the value does not need to be dropped.
56    #[track_caller]
57    fn drop(&mut self) {
58        if !mem::needs_drop::<T>() || self.thread_id == std::thread::current().id() {
59            unsafe {
60                ManuallyDrop::drop(&mut self.value);
61            }
62        } else {
63            invalid_thread();
64        }
65    }
66}
67
68impl<T> ThreadSafeJsValue<T> {
69    /// Checks if the ThreadSafeJsValue is valid for the current thread.
70    ///
71    /// # Panics
72    ///
73    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
74    #[track_caller]
75    fn check_thread(&self) {
76        // This is only needed for non-wasm32 targets.
77        // wasm32 targets are single threaded.
78        #[cfg(not(target_arch = "wasm32"))]
79        if self.thread_id != std::thread::current().id() {
80            invalid_thread();
81        }
82    }
83
84    /// Checks if the ThreadSafeJsValue is valid for the current thread.
85    pub fn is_valid(&self) -> bool {
86        self.thread_id == std::thread::current().id()
87    }
88
89    /// Gets the value from the ThreadSafeJsValue.
90    ///
91    /// # Panics
92    ///
93    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
94    #[track_caller]
95    pub fn value(&self) -> &T {
96        self.check_thread();
97        &self.value
98    }
99
100    /// Tries to get the value from the ThreadSafeJsValue.
101    #[track_caller]
102    pub fn try_value(&self) -> Result<&T, std::io::Error> {
103        if self.thread_id == std::thread::current().id() {
104            Ok(&self.value)
105        } else {
106            Err(std::io::Error::new(
107                std::io::ErrorKind::Other,
108                NOT_ON_CURRENT_THREAD,
109            ))
110        }
111    }
112}
113
114/// # Safety
115/// We assert that the thread_id is the same as the current thread_id
116/// when we dereference the value.
117unsafe impl<T> Send for ThreadSafeJsValue<T> {}
118
119/// # Safety
120/// We assert that the thread_id is the same as the current thread_id
121/// when we dereference the value.
122unsafe impl<T> Sync for ThreadSafeJsValue<T> {}
123
124impl<T> Clone for ThreadSafeJsValue<T>
125where
126    T: Clone,
127{
128    /// Clones the ThreadSafeJsValue.
129    fn clone(&self) -> Self {
130        Self {
131            value: self.value.clone(),
132            thread_id: self.thread_id,
133        }
134    }
135}
136
137impl<T> std::fmt::Debug for ThreadSafeJsValue<T>
138where
139    T: std::fmt::Debug,
140{
141    /// Formats the value for debugging.
142    ///
143    /// # Panics
144    ///
145    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.write_fmt(format_args!("ThreadId:{:?}", self.thread_id))?;
148        f.write_fmt(format_args!("Value:{:?}", self.value.deref()))
149    }
150}
151
152impl<T> std::fmt::Display for ThreadSafeJsValue<T>
153where
154    T: std::fmt::Display,
155{
156    /// Formats the value.
157    ///
158    /// # Panics
159    ///
160    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        self.value.deref().fmt(f)
163    }
164}
165
166impl<T> PartialEq for ThreadSafeJsValue<T>
167where
168    T: PartialEq,
169{
170    /// Compares the value for equality.
171    ///
172    /// # Panics
173    ///
174    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
175    fn eq(&self, other: &Self) -> bool {
176        self.value.deref() == other.value.deref()
177    }
178}
179
180impl<T> Eq for ThreadSafeJsValue<T> where T: Eq {}
181
182impl<T> std::hash::Hash for ThreadSafeJsValue<T>
183where
184    T: std::hash::Hash,
185{
186    /// Hashes the value.
187    ///
188    /// # Panics
189    ///
190    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
191    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
192        self.value.deref().hash(state)
193    }
194}
195
196impl<T> std::cmp::PartialOrd for ThreadSafeJsValue<T>
197where
198    T: std::cmp::PartialOrd,
199{
200    /// Compares the value for ordering.
201    ///
202    /// # Panics
203    ///
204    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
205    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
206        self.value.deref().partial_cmp(other.value.deref())
207    }
208}
209
210impl<T> std::cmp::Ord for ThreadSafeJsValue<T>
211where
212    T: std::cmp::Ord,
213{
214    /// Compares the value for ordering.
215    ///
216    /// # Panics
217    ///
218    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
219    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
220        self.value.deref().cmp(other.value.deref())
221    }
222}
223
224impl<T> std::ops::Deref for ThreadSafeJsValue<T> {
225    type Target = T;
226
227    /// Dereferences the value.
228    ///
229    /// # Panics
230    ///
231    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
232    #[track_caller]
233    fn deref(&self) -> &Self::Target {
234        self.check_thread();
235        &self.value
236    }
237}
238
239impl<T> std::ops::DerefMut for ThreadSafeJsValue<T> {
240    /// Dereferences the value as a mutable reference.
241    ///
242    /// # Panics
243    ///
244    /// Panics if the ThreadSafeJsValue is not valid for the current thread.
245    #[track_caller]
246    fn deref_mut(&mut self) -> &mut Self::Target {
247        self.check_thread();
248        &mut self.value
249    }
250}
251
252/// A trait for converting a value into a ThreadSafeJsValue.
253///
254/// This is useful for converting values that are not Send or Sync.
255/// When they don't have the From trait implemented for ThreadSafeJsValue.
256pub trait IntoThreadSafeJsValue: Sized {
257    /// Converts the value into a ThreadSafeJsValue.
258    fn into_thread_safe_js_value(self) -> ThreadSafeJsValue<Self>
259    where
260        Self: IntoWasmAbi;
261}
262
263impl<T> IntoThreadSafeJsValue for T
264where
265    T: IntoWasmAbi,
266{
267    fn into_thread_safe_js_value(self) -> ThreadSafeJsValue<Self> {
268        ThreadSafeJsValue::new(self)
269    }
270}
271
272#[cold]
273#[track_caller]
274#[inline(never)]
275fn invalid_thread() -> ! {
276    panic!("{}", NOT_ON_CURRENT_THREAD);
277}
278
279/// This is a helper macro to implement the From trait for ThreadSafeJsValue.
280///
281/// This also adds a type alias for the ThreadSafeJsValue with a suffix.
282/// e.g. JsValue -> JsValueTS
283#[allow(dead_code)]
284#[macro_export]
285macro_rules! impl_thread_safe_js_value {
286    ($type:ty) => {
287        impl From<$type> for ThreadSafeJsValue<$type> {
288            fn from(value: $type) -> Self {
289                Self::new(value)
290            }
291        }
292        paste! {pub type [<$type TS>] = ThreadSafeJsValue<$type>;}
293    };
294    ($type:ty, $suffix:expr) => {
295        impl From<$type> for ThreadSafeJsValue<$type> {
296            fn from(value: $type) -> Self {
297                Self::new(value)
298            }
299        }
300        paste! {pub type [< $type $suffix>] = ThreadSafeJsValue<$type>;}
301    };
302}
303
304impl_thread_safe_js_value!(JsValue);
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    #[cfg(target_arch = "wasm32")]
310    use wasm_bindgen::JsValue;
311    #[cfg(target_arch = "wasm32")]
312    use wasm_bindgen_test::wasm_bindgen_test;
313
314    #[cfg(target_arch = "wasm32")]
315    #[wasm_bindgen_test]
316    fn test_thread_safe_js_value() {
317        let js_value = JsValue::from(42);
318        let thread_safe_js_value = ThreadSafeJsValue::new(js_value);
319        assert_eq!(thread_safe_js_value.value(), &JsValue::from(42));
320    }
321
322    #[cfg(target_arch = "wasm32")]
323    #[wasm_bindgen_test]
324    fn test_thread_safe_js_value_clone() {
325        let js_value = JsValue::from(42);
326        let thread_safe_js_value = ThreadSafeJsValue::new(js_value);
327        let cloned_thread_safe_js_value = thread_safe_js_value.clone();
328        assert_eq!(cloned_thread_safe_js_value.value(), &JsValue::from(42));
329    }
330
331    #[cfg(target_arch = "wasm32")]
332    #[wasm_bindgen_test]
333    fn test_thread_safe_js_value_try_value() {
334        let js_value = JsValue::from(42);
335        let thread_safe_js_value = ThreadSafeJsValue::new(js_value);
336        assert_eq!(
337            thread_safe_js_value.try_value().unwrap(),
338            &JsValue::from(42)
339        );
340    }
341
342    #[cfg(target_arch = "wasm32")]
343    #[wasm_bindgen_test]
344    fn test_thread_into_thread_safe_js_value() {
345        let js_value = JsValue::from(42);
346        let thread_safe_js_value = js_value.into_thread_safe_js_value();
347        assert_eq!(thread_safe_js_value.value(), &JsValue::from(42));
348    }
349
350    #[test]
351    fn test_thread_safe_value() {
352        let value = 42;
353        let thread_safe_value = ThreadSafeJsValue::new(value);
354        assert_eq!(thread_safe_value.value(), &42);
355    }
356
357    #[test]
358    fn test_thread_safe_value_clone() {
359        let value = 42;
360        let thread_safe_value = ThreadSafeJsValue::new(value);
361        let cloned_thread_safe_value = thread_safe_value.clone();
362        assert_eq!(cloned_thread_safe_value.value(), &42);
363    }
364
365    #[test]
366    fn test_thread_safe_value_try_value() {
367        let value = 42;
368        let thread_safe_value = ThreadSafeJsValue::new(value);
369        assert_eq!(thread_safe_value.try_value().unwrap(), &42);
370    }
371
372    #[test]
373    fn test_into_thread_safe_value() {
374        let value = 42;
375        let thread_safe_value = value.into_thread_safe_js_value();
376        assert_eq!(thread_safe_value.value(), &42);
377    }
378}