Skip to main content

workflow_wasm/
callback.rs

1//!
2//! [`callback`](self) module provides [`Callback`] struct that helps registering
3//! Rust closures as JavaScript callbacks.
4//!
5
6use js_sys::Function;
7use std::{
8    collections::HashMap,
9    sync::{Arc, Mutex, MutexGuard},
10};
11use thiserror::Error;
12use wasm_bindgen::{
13    JsCast, JsValue,
14    closure::{Closure, IntoWasmClosure, WasmClosure},
15    convert::{FromWasmAbi, ReturnWasmAbi},
16};
17use workflow_core::id::Id;
18
19/// `u64`-based Callback Id (alias of [`workflow_core::id::Id`]).
20pub type CallbackId = Id;
21
22/// Errors produced by the [`callback`](self) module
23#[derive(Error, Debug, Clone)]
24pub enum CallbackError {
25    /// Custom error message
26    #[error("String {0:?}")]
27    String(String),
28
29    /// Error contains a JsValue
30    #[error("JsValue {0:?}")]
31    JsValue(Printable),
32
33    /// LockError message resulting from Mutex lock failure ([`std::sync::PoisonError`])
34    #[error("LockError: Unable to lock closure, {0:?}")]
35    LockError(String),
36
37    #[error("ClosureNotIntialized, Please use `callback.set_closure()`")]
38    /// Results from trying to access a closure value when the closure is not initialized.
39    ClosureNotInitialized,
40}
41
42unsafe impl Send for CallbackError {}
43unsafe impl Sync for CallbackError {}
44
45impl From<JsValue> for CallbackError {
46    fn from(value: JsValue) -> Self {
47        CallbackError::JsValue(value.into())
48    }
49}
50
51impl From<CallbackError> for JsValue {
52    fn from(err: CallbackError) -> Self {
53        JsValue::from_str(&err.to_string())
54    }
55}
56
57impl From<String> for CallbackError {
58    fn from(str: String) -> Self {
59        Self::String(str)
60    }
61}
62
63/// Result type returned by [`callback`](self) module operations, using [`CallbackError`].
64pub type CallbackResult<T> = std::result::Result<T, CallbackError>;
65
66/// Callback Closure that produces a [`wasm_bindgen::JsValue`] error
67pub type CallbackClosure<T> = dyn FnMut(T) -> std::result::Result<(), JsValue>;
68/// Callback Closure that yields no [`std::result::Result`]
69pub type CallbackClosureWithoutResult<T> = dyn FnMut(T);
70
71/// Trait allowing to bind a generic [`Callback`] struct
72/// with a [`CallbackId`] identifier.
73pub trait AsCallback: Send + Sync {
74    /// Returns the unique [`CallbackId`] identifying this callback.
75    fn get_id(&self) -> CallbackId;
76    /// Returns the underlying JavaScript [`Function`] bound to this callback.
77    fn get_fn(&self) -> &Function;
78}
79
80///
81/// [`Callback`] is a struct that owns a given Rust closure
82/// meant to be bound to JavaScript as a callback.
83///
84pub struct Callback<T: ?Sized> {
85    id: CallbackId,
86    closure: Arc<Mutex<Option<Arc<Closure<T>>>>>,
87    closure_js_value: JsValue,
88}
89
90unsafe impl<T: ?Sized> Send for Callback<T> {}
91unsafe impl<T: ?Sized> Sync for Callback<T> {}
92
93impl<T: ?Sized> std::fmt::Debug for Callback<T> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "Callback{{ id:\"{}\" }}", self.id)
96    }
97}
98
99impl<T> AsCallback for Callback<T>
100where
101    T: ?Sized + WasmClosure + 'static,
102{
103    fn get_id(&self) -> CallbackId {
104        self.id
105    }
106
107    fn get_fn(&self) -> &Function {
108        let f: &Function = self.as_ref();
109        f //.clone()
110    }
111}
112
113impl<T: ?Sized> Clone for Callback<T> {
114    fn clone(&self) -> Self {
115        Self {
116            id: self.id,
117            closure: self.closure.clone(),
118            closure_js_value: self.closure_js_value.clone(),
119        }
120    }
121}
122
123impl<T> Default for Callback<T>
124where
125    T: ?Sized + WasmClosure + 'static,
126{
127    fn default() -> Self {
128        Self {
129            id: CallbackId::new(),
130            closure: Arc::new(Mutex::new(None)),
131            closure_js_value: JsValue::null(),
132        }
133    }
134}
135
136macro_rules! create_fns {
137    ($(
138        ($name: ident, $($var:ident)*)
139    )*) => ($(
140        /// Create a new [`Callback`] from a closure taking a fixed number of arguments.
141        pub fn $name<$($var,)* R>(callback:T)->Callback<dyn FnMut($($var,)*)->R>
142        where
143            T: 'static + FnMut($($var,)*)->R,
144            $($var: FromWasmAbi + 'static,)*
145            R: ReturnWasmAbi + 'static
146        {
147            Callback::create(callback)
148        }
149
150    )*)
151}
152
153impl<T> Callback<T> {
154    create_fns! {
155        (new_with_args_0, )
156        (new_with_args_1, A)
157        (new_with_args_2, A B)
158        (new_with_args_3, A B C)
159        (new_with_args_4, A B C D)
160        (new_with_args_5, A B C D E)
161        (new_with_args_6, A B C D E F)
162        (new_with_args_7, A B C D E F G)
163        (new_with_args_8, A B C D E F G H)
164    }
165
166    /// Create a new [`Callback`] instance with the given closure.
167    pub fn new<A, R>(callback: T) -> Callback<dyn FnMut(A) -> R>
168    where
169        T: 'static + FnMut(A) -> R,
170        A: FromWasmAbi + 'static,
171        R: ReturnWasmAbi + 'static,
172    {
173        Callback::create(callback)
174    }
175}
176
177impl<T> Callback<T>
178where
179    T: ?Sized + WasmClosure + 'static,
180{
181    /// Create a new [`Callback`] instance with the given closure.
182    pub fn create<F>(t: F) -> Self
183    where
184        F: IntoWasmClosure<T> + 'static,
185    {
186        let mut callback = Callback::<T>::default();
187        callback.set_closure(t);
188
189        callback
190    }
191
192    /// Set closure in the given [`Callback`] instance.
193    pub fn set_closure<F>(&mut self, t: F)
194    where
195        F: IntoWasmClosure<T> + 'static,
196    {
197        let closure = Closure::new(t);
198        let closure_js_value = closure.as_ref().clone();
199
200        *self.closure.lock().unwrap() = Some(Arc::new(closure));
201        self.closure_js_value = closure_js_value;
202    }
203
204    /// Obtain a [`wasm_bindgen::JsCast`] value for this callback.
205    pub fn into_js<J>(&self) -> &J
206    where
207        J: JsCast,
208    {
209        self.closure_js_value.as_ref().unchecked_ref()
210    }
211
212    /// Obtain an [`std::sync::Arc`] of the given closure.
213    /// Returns [`CallbackError::ClosureNotInitialized`] if the closure is `None`.
214    pub fn closure(&self) -> CallbackResult<Arc<Closure<T>>> {
215        match self.closure.lock() {
216            Ok(locked) => match locked.as_ref() {
217                Some(c) => Ok(c.clone()),
218                None => Err(CallbackError::ClosureNotInitialized),
219            },
220            Err(err) => Err(CallbackError::LockError(err.to_string())),
221        }
222    }
223}
224
225impl<T> AsRef<JsValue> for Callback<T>
226where
227    T: ?Sized + WasmClosure + 'static,
228{
229    fn as_ref(&self) -> &JsValue {
230        self.closure_js_value.as_ref().unchecked_ref()
231    }
232}
233
234impl<T> From<Callback<T>> for JsValue
235where
236    T: ?Sized + WasmClosure + 'static,
237{
238    fn from(callback: Callback<T>) -> Self {
239        callback.closure_js_value.unchecked_into()
240    }
241}
242
243impl<T> AsRef<js_sys::Function> for Callback<T>
244where
245    T: ?Sized + WasmClosure + 'static,
246{
247    fn as_ref(&self) -> &js_sys::Function {
248        self.closure_js_value.as_ref().unchecked_ref()
249    }
250}
251
252// impl<T> From<Callback<T>> for Arc<dyn AsCallback>
253// where
254//     T: ?Sized + WasmClosure + 'static {
255//     fn from(callback: Callback<T>) -> Self {
256//         Arc::new(callback)
257//     }
258// }
259
260// macro_rules! callbackmap_from {
261//     ($(
262//         ($($var:ident)*)
263//     )*) => ($(
264//         From<$($var,)* R> for CallbackMap
265//         where
266//         {
267
268//         }
269
270//     )*)
271// }
272
273/// Collection of callbacks contained in a [`std::collections::HashMap`].
274#[derive(Clone)]
275pub struct CallbackMap {
276    inner: Arc<Mutex<HashMap<CallbackId, Arc<dyn AsCallback>>>>,
277}
278
279impl std::fmt::Debug for CallbackMap {
280    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281        write!(f, "CallbackMap{{...}}")
282    }
283}
284
285impl Default for CallbackMap {
286    fn default() -> Self {
287        Self::new()
288    }
289}
290
291impl CallbackMap {
292    /// Create a new [`CallbackMap`] instance.
293    pub fn new() -> Self {
294        Self {
295            inner: Arc::new(Mutex::new(HashMap::new())),
296        }
297    }
298
299    /// Remove all callbacks from the collection.
300    pub fn clear(&self) {
301        self.inner.lock().unwrap().clear();
302    }
303
304    /// Get access to the [`std::sync::MutexGuard`] owning the inner [`std::collections::HashMap`].
305    pub fn inner(&self) -> MutexGuard<'_, HashMap<CallbackId, Arc<dyn AsCallback>>> {
306        self.inner.lock().unwrap()
307    }
308
309    /// Insert a new callback into the collection
310    pub fn retain<L>(&self, callback: L) -> CallbackResult<Option<Arc<dyn AsCallback>>>
311    where
312        L: Sized + AsCallback + 'static,
313    {
314        let id = callback.get_id();
315
316        let v = self
317            .inner
318            .lock()
319            .map_err(|err| CallbackError::LockError(err.to_string()))?
320            .insert(id, Arc::new(callback));
321
322        Ok(v)
323    }
324
325    /// Remove a callback from the collection
326    pub fn remove(&self, id: &CallbackId) -> CallbackResult<Option<Arc<dyn AsCallback>>> {
327        let v = self
328            .inner
329            .lock()
330            .map_err(|err| CallbackError::LockError(err.to_string()))?
331            .remove(id);
332        Ok(v)
333    }
334}
335
336///
337/// creates a [Callback] instance
338/// by inspecting a given closure signature
339///
340/// ```
341/// // include dependencies
342/// use workflow_wasm::prelude::*;
343/// ```
344/// <div class="example-wrap compile_fail"><pre class="compile_fail" style="white-space:normal;font:inherit;">
345///
346/// **Warning**: the [`macro@callback`] macro expects to receive a closure as an argument
347/// and will use this closure's signature to determine which [`Callback`] binding function
348/// to use.  However, if you declare a closure as a variable and then try to pass
349/// it to the [`macro@callback`] macro, the macro will fail with an error as follows:
350/// "closure is expected to take 1 argument"
351///
352/// </pre></div>
353///
354/// - #### If passing closure as variable, it will accept only 1 argument:
355/// ```no_compile
356/// let closure_as_variable = |value:bool|{
357///     ...
358/// };
359/// let callback = callback!(closure_as_variable);
360/// ```
361/// The above code will create callback like this:
362/// ```no_compile
363/// let callback = Callback::new(closure_as_variable);
364/// ```
365///
366/// - #### Examples of incorrect use:
367///
368/// ```compile_fail
369/// // 2 arguments
370/// let closure_as_variable = |arg1:bool, arg2:u16|{
371///     //...
372/// };
373/// let callback = callback!(closure_as_variable);
374/// ```
375///
376/// ```compile_fail
377/// // no arguments
378/// let closure_as_variable = ||{
379///     //...
380/// };
381/// let callback = callback!(closure_as_variable);
382/// ```
383///
384/// - #### If you have closure variable with more or less than 1 argument, you can use on the the following direct methods:
385///     - [Callback::new_with_args_0]
386///     - [Callback::new_with_args_1]
387///     - [Callback::new_with_args_2]
388///     - [Callback::new_with_args_3]
389///     - [Callback::new_with_args_4]
390///     - [Callback::new_with_args_5]
391///     - [Callback::new_with_args_6]
392///     - [Callback::new_with_args_7]
393///     - [Callback::new_with_args_8]
394///
395/// - #### A closure supplied directly to the [`callback`] macro can accept 0-8 arguments:
396///     ```no_compile
397///     let callback = callback!(|value:bool|{
398///         //
399///     });
400///     ```
401///     Output will be as follows:
402///     ```no_compile
403///     let callback = Callback::new_with_args_1(|value:bool|{
404///         //
405///     });
406///     ```
407/// - ##### Example of a closure with 2 arguments:
408///     ```no_compile
409///     let callback = callback!(|arg1:u16, value:bool|{
410///         //
411///     });
412///     ```
413///     Output will be as follows:
414///     ```no_compile
415///     let callback = Callback::new_with_args_2(|arg1:u16, value:bool|{
416///        //
417///     });
418///     ```
419///
420pub use workflow_wasm_macros::callback;
421
422use crate::printable::Printable;