dioxus_use_js/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3
4use std::ops::{Deref, DerefMut};
5use std::pin::Pin;
6use std::task::{Context, Poll};
7use std::{error::Error, fmt::Display, sync::Arc};
8
9#[cfg(feature = "build")]
10mod build;
11#[cfg(feature = "build")]
12pub use build::*;
13
14use dioxus::document::{self, Eval};
15pub use dioxus_use_js_macro::use_js;
16
17// We export these so downstreams don't need `serde` or `serde_json` directly
18// exports used by macro.
19#[doc(hidden)]
20pub use serde::Serialize as SerdeSerialize;
21#[doc(hidden)]
22pub use serde::de::DeserializeOwned as SerdeDeDeserializeOwned;
23#[doc(hidden)]
24pub use serde::de::Error as SerdeDeError;
25#[doc(hidden)]
26pub use serde_json::Error as SerdeJsonError;
27#[doc(hidden)]
28pub use serde_json::Value as SerdeJsonValue;
29#[doc(hidden)]
30pub use serde_json::from_value as serde_json_from_value;
31#[doc(hidden)]
32pub const __CALLBACK_SEND_VALIDATION_MSG: &str =
33    "Callbacks should always send back a value that is an array of three.";
34#[doc(hidden)]
35pub const __RESULT_SEND_VALIDATION_MSG: &str =
36    "Result should always send back a value that is an array of two.";
37#[doc(hidden)]
38pub const __INDEX_VALIDATION_MSG: &str = "The index value was an unexpected type";
39#[doc(hidden)]
40pub const __BAD_CALL_MSG: &str = "Should only attempt to call known actions.";
41#[doc(hidden)]
42pub const __BAD_VOID_RETURN: &str =
43    "A function that should return no value instead returned a value";
44#[doc(hidden)]
45pub const __UNEXPECTED_CALLBACK_TYPE: &str = "The callback was called with the wrong type";
46// We do not export this so the dioxus version doing the eval is the same, otherwise it may compile but using two different versions of dioxus at runtime will likely cause a runtime error
47// be two different versions of dioxus in the graph
48// pub use dioxus::document::eval as dioxus_document_eval;
49// pub use dioxus::document::EvalError as DioxusEvalError;
50
51//************************************************************************//
52
53fn _send_sync_error_assert() {
54    fn is_send<T: Send>(_: &T) {}
55    fn is_sync<T: Sync>(_: &T) {}
56    fn is_error<T: Error>(_: &T) {}
57
58    let o: JsError = JsError::Threw { func: "" };
59    is_send(&o);
60    is_sync(&o);
61    is_error(&o);
62}
63
64/// An error related to the execution of a javascript operation
65#[derive(Debug)]
66pub enum JsError {
67    /// Error occurred during dioxus evalution.
68    /// If this occurs, it usually mean your js is not valid or the wrong type was returned from
69    /// your js function
70    Eval {
71        /// The name of the js function
72        func: &'static str,
73        error: dioxus::document::EvalError,
74    },
75    /// A js function that threw a value during execution. The actual error value is logged on the js side as a warning.
76    Threw {
77        /// Name of the js function
78        func: &'static str,
79    },
80}
81
82impl std::fmt::Display for JsError {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            JsError::Eval { func: name, error } => {
86                write!(f, "JavaScript function '{}' eval error: {}", name, error)
87            }
88            JsError::Threw { func: name } => {
89                write!(
90                    f,
91                    "JavaScript function '{}' threw an error during execution",
92                    name
93                )
94            }
95        }
96    }
97}
98
99impl std::error::Error for JsError {}
100
101//************************************************************************//
102
103/// A reference to a javascript value that can be held on the dioxus side and passed to functions generated
104/// by this crate.
105///
106/// An instance of this is created or used by e.g.
107/// ```rust,ignore
108/// dioxus_use_js::use_js!("ts/example.ts", "assets/example.js"::usingJsValue);
109/// ```
110/// Where `"ts/example.ts"` uses this marker type
111/// ```ts
112/// type JsValue<T = any> = T;
113/// ```
114///
115/// This uses `Arc` internally and the value on the js side is destroyed when the last reference is dropped
116// Dev Note: No `serde::Serialize` or `serde::Deserialize` on purpose since the value is destroyed when dropped
117#[derive(
118    serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
119)]
120pub struct JsValue(Arc<JsValueInner>);
121
122/// Abstraction used to implement the one time drop
123#[derive(
124    serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash,
125)]
126struct JsValueInner(String);
127
128impl JsValue {
129    #[deprecated(note = "This constructor is for internal use only. Do not use directly.")]
130    #[doc(hidden)]
131    pub fn internal_create(id: String) -> Self {
132        Self(Arc::new(JsValueInner(id)))
133    }
134
135    #[deprecated(note = "This is for internal use only. Do not use directly.")]
136    #[doc(hidden)]
137    pub fn internal_get(&self) -> &str {
138        self.0.0.as_str()
139    }
140}
141
142impl Display for JsValue {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        write!(f, "Value in js: window[\"{}\"]", self.0.0)
145    }
146}
147
148impl Drop for JsValueInner {
149    fn drop(&mut self) {
150        let object_name = std::mem::take(&mut self.0);
151        // work around for no async drop trait
152        dioxus::core::spawn_forever(async move {
153            let eval =
154                dioxus::document::eval(&format!("delete window[\"{object_name}\"];return null;"));
155            if let Err(error) = eval.await {
156                dioxus::logger::tracing::error!(
157                    "Failed to clean up JavaScript object `window[\"{object_name}\"]`. Error: {error}"
158                );
159            } else {
160                dioxus::logger::tracing::trace!(
161                    "Cleaned up JavaScript object `window[\"{object_name}\"]`."
162                );
163            }
164        });
165    }
166}
167
168/// When a normal Eval drops. It does not signal to the channel that it has been dropped. Thus any `await dioxus.recv()`
169/// will be awaiting forever. Thus we only use one `await dioxus.recv()` after all the parameters have been sent to
170/// signal drop. This struct will send the value to resolve that promise on drop.
171#[doc(hidden)]
172pub struct EvalDrop(document::Eval);
173
174impl EvalDrop {
175    /// Create a new EvalDrop from a dioxus eval
176    pub fn new(eval: document::Eval) -> Self {
177        Self(eval)
178    }
179}
180
181impl Deref for EvalDrop {
182    type Target = document::Eval;
183
184    fn deref(&self) -> &Self::Target {
185        &self.0
186    }
187}
188
189impl DerefMut for EvalDrop {
190    fn deref_mut(&mut self) -> &mut Self::Target {
191        &mut self.0
192    }
193}
194
195impl Drop for EvalDrop {
196    fn drop(&mut self) {
197        if let Err(e) = self.0.send(serde_json::Value::Null) {
198            dioxus::logger::tracing::error!("Failed to notify about Eval drop: {}", e);
199        }
200    }
201}
202
203/// Used in generated code.
204#[doc(hidden)]
205#[derive(Clone)]
206pub struct CallbackResponder(Arc<CallbackResponderInner>);
207
208struct CallbackResponderInner(Eval);
209
210impl CallbackResponder {
211    pub fn new(invocation_id: &str) -> Self {
212        // r = [id, ok, data], i = id, o = ok, d = data, f = window[function_id], x = f[id] = [resolve, reject]
213        CallbackResponder(Arc::new(CallbackResponderInner(dioxus::document::eval(
214            &format!(
215                "while(true){{let r=await dioxus.recv();if(!Array.isArray(r))break;let f=window[\"{invocation_id}\"];if(f==null)break;let i=r[0],o=r[1],d=r[2],x=f[i];delete f[i];if(o)x[0](d);else x[1](d);}}",
216            ),
217        ))))
218    }
219
220    pub fn respond<T: serde::Serialize>(&self, request_id: u64, is_ok: bool, data: T) {
221        let payload = (request_id, is_ok, data);
222
223        let result = self.0.0.send(payload);
224        if let Err(e) = result {
225            dioxus::logger::tracing::error!(
226                "Failed to send callback response for invocation '{}' request '{}': {}",
227                request_id,
228                is_ok,
229                e
230            );
231        }
232    }
233}
234
235impl Drop for CallbackResponderInner {
236    fn drop(&mut self) {
237        // Send a non-array value to break the loop
238        let result = self.0.send(serde_json::Value::Null);
239        if let Err(e) = result {
240            dioxus::logger::tracing::error!("Failed to shut down callback responder: {}", e);
241        }
242    }
243}
244
245#[doc(hidden)]
246pub struct PendingFuture;
247
248impl Future for PendingFuture {
249    type Output = ();
250
251    fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
252        Poll::Pending
253    }
254}
255
256// pub struct JsNotifyOnce(Arc<JsNotifyOnceInner>);
257
258// impl JsNotifyOnce {
259//     pub fn new(notify_on_drop: bool) -> Self {
260//         static NOTIFY_ID_COUNTER: std::sync::atomic::AtomicU64 =
261//             std::sync::atomic::AtomicU64::new(0);
262//         let id = format!(
263//             "__js-notify-{}",
264//             NOTIFY_ID_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
265//         );
266//         dioxus::document::eval(&format!(
267//             "let r;let p=new Promise((res)=>{{r=res;}});window[\"{id}-p\"]=p;window[\"{id}-r\"]=r;"
268//         ));
269//         Self(Arc::new(JsNotifyOnceInner {
270//             id,
271//             notify_on_drop,
272//             is_notified: AtomicBool::new(false),
273//         }))
274//     }
275
276//     pub fn notify(&self) {
277//         self.0.notify();
278//     }
279
280//     fn id(&self) -> &str {
281//         &self.0.id
282//     }
283// }
284
285// struct JsNotifyOnceInner {
286//     id: String,
287//     notify_on_drop: bool,
288//     is_notified: AtomicBool,
289// }
290
291// impl JsNotifyOnceInner {
292//     fn notify(&self) {
293//         if self
294//             .is_notified
295//             .swap(true, std::sync::atomic::Ordering::AcqRel)
296//         {
297//             return;
298//         }
299//         let id = self.id.clone();
300//         dioxus::core::spawn_forever(async move {
301//             let eval = dioxus::document::eval(&format!(
302//                 "delete window[\"{id}-p\"];let r=window[\"{id}-r\"];if(r==undefined){{return null;}}r.resolve();delete window[\"{id}-r\"];return null;"
303//             ));
304//             if let Err(error) = eval.await {
305//                 dioxus::logger::tracing::error!(
306//                     "Failed to notify JavaScript object `window[\"{id}\"]`. Error: {error}"
307//                 );
308//             } else {
309//                 dioxus::logger::tracing::trace!("Notified JavaScript object `window[\"{id}\"]`.");
310//             }
311//         });
312//     }
313// }
314
315// impl Drop for JsNotifyOnceInner {
316//     fn drop(&mut self) {
317//         if self.notify_on_drop {
318//             self.notify();
319//         }
320//     }
321// }