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