1use std::error::Error;
9use std::fmt;
10
11use serde::de::DeserializeOwned;
12
13use crate::ScopeId;
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct ClientModuleError {
17 message: String,
18}
19
20impl ClientModuleError {
21 pub fn new(message: impl Into<String>) -> Self {
22 Self {
23 message: message.into(),
24 }
25 }
26
27 pub fn message(&self) -> &str {
28 &self.message
29 }
30}
31
32impl fmt::Display for ClientModuleError {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.write_str(&self.message)
35 }
36}
37
38impl Error for ClientModuleError {}
39
40#[derive(Clone, Debug)]
41pub struct ClientModule {
42 name: String,
43 #[cfg(target_arch = "wasm32")]
44 value: wasm_bindgen::JsValue,
45}
46
47impl ClientModule {
48 pub fn required(name: impl Into<String>) -> Result<Self, ClientModuleError> {
49 let name = name.into();
50 #[cfg(not(target_arch = "wasm32"))]
51 {
52 Err(ClientModuleError::new(format!(
53 "client module `{name}` is only available in the browser"
54 )))
55 }
56 #[cfg(target_arch = "wasm32")]
57 {
58 Self::optional(&name)?.ok_or_else(|| {
59 ClientModuleError::new(format!("client module `{name}` is not registered"))
60 })
61 }
62 }
63
64 pub fn optional(name: impl Into<String>) -> Result<Option<Self>, ClientModuleError> {
65 let name = name.into();
66 optional_impl(name)
67 }
68
69 pub fn name(&self) -> &str {
70 &self.name
71 }
72
73 pub async fn call_async<T>(&self, method: impl AsRef<str>) -> Result<T, ClientModuleError>
74 where
75 T: DeserializeOwned,
76 {
77 call_async_impl(self, method.as_ref()).await
78 }
79
80 pub fn subscribe<T>(
81 &self,
82 scope: ScopeId,
83 method: impl AsRef<str>,
84 handler: impl FnMut(Result<T, ClientModuleError>) + 'static,
85 ) -> Result<(), ClientModuleError>
86 where
87 T: DeserializeOwned + 'static,
88 {
89 subscribe_impl(self, scope, method.as_ref(), handler)
90 }
91}
92
93#[cfg(target_arch = "wasm32")]
94mod wasm {
95 use std::cell::RefCell;
96 use std::collections::HashMap;
97 use std::rc::Rc;
98
99 use js_sys::{Function, Promise, Reflect};
100 use wasm_bindgen::closure::Closure;
101 use wasm_bindgen::{JsCast, JsValue};
102 use wasm_bindgen_futures::{spawn_local, JsFuture};
103
104 use super::{ClientModule, ClientModuleError, DeserializeOwned, ScopeId};
105
106 const REGISTRY_NAME: &str = "__pp_client_modules";
107
108 thread_local! {
109 static SUBSCRIPTIONS: RefCell<HashMap<ScopeId, Vec<ClientModuleSubscription>>> =
110 RefCell::new(HashMap::new());
111 }
112
113 struct ClientModuleSubscription {
114 unsubscribe: Option<Function>,
115 _callback: Closure<dyn FnMut(JsValue)>,
116 }
117
118 impl Drop for ClientModuleSubscription {
119 fn drop(&mut self) {
120 if let Some(unsubscribe) = self.unsubscribe.take() {
121 let _ = unsubscribe.call0(&JsValue::NULL);
122 }
123 }
124 }
125
126 pub(super) fn optional_impl(name: String) -> Result<Option<ClientModule>, ClientModuleError> {
127 let global = js_sys::global();
128 let registry =
129 Reflect::get(&global, &JsValue::from_str(REGISTRY_NAME)).map_err(|value| {
130 ClientModuleError::new(js_value_message(value, "read client modules"))
131 })?;
132 if registry.is_null() || registry.is_undefined() {
133 return Ok(None);
134 }
135
136 let value = Reflect::get(®istry, &JsValue::from_str(&name)).map_err(|value| {
137 ClientModuleError::new(format!(
138 "client module `{name}` could not be read: {}",
139 js_value_message(value, "unknown JavaScript error")
140 ))
141 })?;
142 if value.is_null() || value.is_undefined() {
143 return Ok(None);
144 }
145
146 Ok(Some(ClientModule { name, value }))
147 }
148
149 pub(super) async fn call_async_impl<T>(
150 module: &ClientModule,
151 method: &str,
152 ) -> Result<T, ClientModuleError>
153 where
154 T: DeserializeOwned,
155 {
156 let promise = module
157 .call_raw(method)?
158 .dyn_into::<Promise>()
159 .map_err(|_| {
160 ClientModuleError::new(format!(
161 "client module `{}.{method}` did not return a Promise",
162 module.name
163 ))
164 })?;
165 let value = JsFuture::from(promise).await.map_err(|value| {
166 ClientModuleError::new(format!(
167 "client module `{}.{method}` failed: {}",
168 module.name,
169 js_value_message(value, "unknown JavaScript error")
170 ))
171 })?;
172 decode_value(&module.name, method, value)
173 }
174
175 pub(super) fn subscribe_impl<T>(
176 module: &ClientModule,
177 scope: ScopeId,
178 method: &str,
179 handler: impl FnMut(Result<T, ClientModuleError>) + 'static,
180 ) -> Result<(), ClientModuleError>
181 where
182 T: DeserializeOwned + 'static,
183 {
184 let method_fn = module.method(method)?;
185 let module_name = module.name.clone();
186 let method_name = method.to_string();
187 let handler = Rc::new(RefCell::new(handler));
188 let callback = Closure::<dyn FnMut(JsValue)>::new(move |value| {
189 let result = decode_value(&module_name, &method_name, value);
190 let handler = handler.clone();
191 spawn_local(async move {
192 handler.borrow_mut()(result);
193 });
194 });
195 let unsubscribe = method_fn
196 .call1(&module.value, callback.as_ref())
197 .map_err(|value| {
198 ClientModuleError::new(format!(
199 "client module `{}.{method}` subscribe failed: {}",
200 module.name,
201 js_value_message(value, "unknown JavaScript error")
202 ))
203 })?
204 .dyn_into::<Function>()
205 .map_err(|_| {
206 ClientModuleError::new(format!(
207 "client module `{}.{method}` did not return an unsubscribe function",
208 module.name
209 ))
210 })?;
211
212 SUBSCRIPTIONS.with(|subscriptions| {
213 subscriptions
214 .borrow_mut()
215 .entry(scope)
216 .or_default()
217 .push(ClientModuleSubscription {
218 unsubscribe: Some(unsubscribe),
219 _callback: callback,
220 });
221 });
222 crate::on_scope_unmount_for(scope, move || {
223 SUBSCRIPTIONS.with(|subscriptions| {
224 subscriptions.borrow_mut().remove(&scope);
225 });
226 });
227
228 Ok(())
229 }
230
231 impl ClientModule {
232 fn call_raw(&self, method: &str) -> Result<JsValue, ClientModuleError> {
233 self.method(method)?.call0(&self.value).map_err(|value| {
234 ClientModuleError::new(format!(
235 "client module `{}.{method}` failed: {}",
236 self.name,
237 js_value_message(value, "unknown JavaScript error")
238 ))
239 })
240 }
241
242 fn method(&self, method: &str) -> Result<Function, ClientModuleError> {
243 Reflect::get(&self.value, &JsValue::from_str(method))
244 .map_err(|value| {
245 ClientModuleError::new(format!(
246 "client module `{}.{method}` could not be read: {}",
247 self.name,
248 js_value_message(value, "unknown JavaScript error")
249 ))
250 })?
251 .dyn_into::<Function>()
252 .map_err(|_| {
253 ClientModuleError::new(format!(
254 "client module `{}.{method}` is not a function",
255 self.name
256 ))
257 })
258 }
259 }
260
261 fn decode_value<T>(module: &str, method: &str, value: JsValue) -> Result<T, ClientModuleError>
262 where
263 T: DeserializeOwned,
264 {
265 serde_wasm_bindgen::from_value(value).map_err(|err| {
266 ClientModuleError::new(format!(
267 "client module `{module}.{method}` returned an invalid payload: {err}"
268 ))
269 })
270 }
271
272 fn js_value_message(value: JsValue, fallback: &str) -> String {
273 if let Some(message) = value.as_string() {
274 return message;
275 }
276 if let Ok(message) = Reflect::get(&value, &JsValue::from_str("message")) {
277 if let Some(message) = message.as_string() {
278 return message;
279 }
280 }
281 fallback.to_string()
282 }
283}
284
285#[cfg(target_arch = "wasm32")]
286use wasm::{call_async_impl, optional_impl, subscribe_impl};
287
288#[cfg(not(target_arch = "wasm32"))]
289fn optional_impl(name: String) -> Result<Option<ClientModule>, ClientModuleError> {
290 let _ = name;
291 Ok(None)
292}
293
294#[cfg(not(target_arch = "wasm32"))]
295async fn call_async_impl<T>(module: &ClientModule, method: &str) -> Result<T, ClientModuleError>
296where
297 T: DeserializeOwned,
298{
299 Err(ClientModuleError::new(format!(
300 "client module `{}.{method}` is only available in the browser",
301 module.name
302 )))
303}
304
305#[cfg(not(target_arch = "wasm32"))]
306fn subscribe_impl<T>(
307 module: &ClientModule,
308 _scope: ScopeId,
309 method: &str,
310 _handler: impl FnMut(Result<T, ClientModuleError>) + 'static,
311) -> Result<(), ClientModuleError>
312where
313 T: DeserializeOwned + 'static,
314{
315 Err(ClientModuleError::new(format!(
316 "client module `{}.{method}` is only available in the browser",
317 module.name
318 )))
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn missing_client_module_message_names_the_module() {
327 let err = ClientModule::required("firebase").unwrap_err();
328 assert_eq!(
329 err.message(),
330 "client module `firebase` is only available in the browser"
331 );
332 }
333}