Skip to main content

clasp_wasm/
lib.rs

1//! Clasp WebAssembly bindings
2//!
3//! This crate provides WebAssembly bindings for Clasp,
4//! enabling browser-based clients.
5
6#[cfg(feature = "p2p")]
7pub mod p2p;
8
9use std::cell::RefCell;
10use std::collections::HashMap;
11use std::rc::Rc;
12use wasm_bindgen::prelude::*;
13use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
14
15use clasp_core::{
16    codec, HelloMessage, Message, SetMessage, SubscribeMessage, SubscribeOptions, Value,
17    PROTOCOL_VERSION, WS_SUBPROTOCOL,
18};
19
20#[cfg(feature = "console_error_panic_hook")]
21pub fn set_panic_hook() {
22    console_error_panic_hook::set_once();
23}
24
25/// Initialize the WASM module
26#[wasm_bindgen(start)]
27pub fn init() {
28    #[cfg(feature = "console_error_panic_hook")]
29    set_panic_hook();
30}
31
32/// Clasp WASM client
33#[wasm_bindgen]
34pub struct ClaspWasm {
35    ws: WebSocket,
36    session_id: Rc<RefCell<Option<String>>>,
37    connected: Rc<RefCell<bool>>,
38    params: Rc<RefCell<HashMap<String, JsValue>>>,
39    on_message: Rc<RefCell<Option<js_sys::Function>>>,
40    on_connect: Rc<RefCell<Option<js_sys::Function>>>,
41    on_disconnect: Rc<RefCell<Option<js_sys::Function>>>,
42    on_error: Rc<RefCell<Option<js_sys::Function>>>,
43    on_auth_error: Rc<RefCell<Option<js_sys::Function>>>,
44    sub_id: Rc<RefCell<u32>>,
45    token: Rc<RefCell<Option<String>>>,
46}
47
48#[wasm_bindgen]
49impl ClaspWasm {
50    /// Create a new Clasp client
51    #[wasm_bindgen(constructor)]
52    pub fn new(url: &str) -> Result<ClaspWasm, JsValue> {
53        Self::new_with_token(url, None)
54    }
55
56    /// Create a new Clasp client with an authentication token
57    #[wasm_bindgen(js_name = newWithToken)]
58    pub fn new_with_token(url: &str, token: Option<String>) -> Result<ClaspWasm, JsValue> {
59        // Create WebSocket with subprotocol
60        let ws = WebSocket::new_with_str(url, WS_SUBPROTOCOL)?;
61        ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
62
63        let client = ClaspWasm {
64            ws,
65            session_id: Rc::new(RefCell::new(None)),
66            connected: Rc::new(RefCell::new(false)),
67            params: Rc::new(RefCell::new(HashMap::new())),
68            on_message: Rc::new(RefCell::new(None)),
69            on_connect: Rc::new(RefCell::new(None)),
70            on_disconnect: Rc::new(RefCell::new(None)),
71            on_error: Rc::new(RefCell::new(None)),
72            on_auth_error: Rc::new(RefCell::new(None)),
73            sub_id: Rc::new(RefCell::new(1)),
74            token: Rc::new(RefCell::new(token)),
75        };
76
77        client.setup_handlers()?;
78
79        Ok(client)
80    }
81
82    /// Set the authentication token (must be called before connection is established)
83    #[wasm_bindgen(js_name = setToken)]
84    pub fn set_token(&self, token: String) {
85        *self.token.borrow_mut() = Some(token);
86    }
87
88    /// Set up WebSocket event handlers
89    fn setup_handlers(&self) -> Result<(), JsValue> {
90        let connected = self.connected.clone();
91        let session_id = self.session_id.clone();
92        let params = self.params.clone();
93        let on_connect = self.on_connect.clone();
94        let on_message = self.on_message.clone();
95        let on_auth_error = self.on_auth_error.clone();
96        let ws = self.ws.clone();
97        let token = self.token.clone();
98
99        // onopen handler
100        let ws_open = ws.clone();
101        let token_open = token.clone();
102        let onopen = Closure::wrap(Box::new(move |_: JsValue| {
103            // Send HELLO with optional token
104            let token_value = token_open.borrow().clone();
105            let hello = Message::Hello(HelloMessage {
106                version: PROTOCOL_VERSION,
107                name: "Clasp WASM Client".to_string(),
108                features: vec![
109                    "param".to_string(),
110                    "event".to_string(),
111                    "stream".to_string(),
112                ],
113                capabilities: None,
114                token: token_value,
115            });
116
117            if let Ok(bytes) = codec::encode(&hello) {
118                let array = js_sys::Uint8Array::from(bytes.as_ref());
119                let _ = ws_open.send_with_array_buffer(&array.buffer());
120            }
121        }) as Box<dyn FnMut(JsValue)>);
122        self.ws.set_onopen(Some(onopen.as_ref().unchecked_ref()));
123        onopen.forget();
124
125        // onmessage handler
126        let connected_msg = connected.clone();
127        let session_msg = session_id.clone();
128        let params_msg = params.clone();
129        let on_connect_msg = on_connect.clone();
130        let on_message_msg = on_message.clone();
131        let on_auth_error_msg = on_auth_error.clone();
132        let ws_msg = ws.clone();
133
134        let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
135            if let Ok(abuf) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
136                let array = js_sys::Uint8Array::new(&abuf);
137                let bytes: Vec<u8> = array.to_vec();
138
139                if let Ok((msg, _)) = codec::decode(&bytes) {
140                    match &msg {
141                        Message::Welcome(welcome) => {
142                            *session_msg.borrow_mut() = Some(welcome.session.clone());
143                            *connected_msg.borrow_mut() = true;
144
145                            if let Some(callback) = on_connect_msg.borrow().as_ref() {
146                                let _ = callback.call0(&JsValue::NULL);
147                            }
148                        }
149                        Message::Error(error) => {
150                            // Handle authentication errors
151                            match error.code {
152                                300 | 302 => {
153                                    // 300 = Unauthorized, 302 = TokenExpired
154                                    // Close the connection and notify
155                                    let _ = ws_msg.close();
156                                    *connected_msg.borrow_mut() = false;
157
158                                    if let Some(callback) = on_auth_error_msg.borrow().as_ref() {
159                                        let error_obj = js_sys::Object::new();
160                                        let _ = js_sys::Reflect::set(
161                                            &error_obj,
162                                            &JsValue::from_str("code"),
163                                            &JsValue::from_f64(error.code as f64),
164                                        );
165                                        let _ = js_sys::Reflect::set(
166                                            &error_obj,
167                                            &JsValue::from_str("message"),
168                                            &JsValue::from_str(&error.message),
169                                        );
170                                        let _ = callback.call1(&JsValue::NULL, &error_obj.into());
171                                    }
172                                }
173                                _ => {
174                                    // Other errors - just log for now
175                                }
176                            }
177                        }
178                        Message::Set(set) => {
179                            let js_value = value_to_js(&set.value);
180                            params_msg
181                                .borrow_mut()
182                                .insert(set.address.clone(), js_value.clone());
183
184                            if let Some(callback) = on_message_msg.borrow().as_ref() {
185                                let _ = callback.call2(
186                                    &JsValue::NULL,
187                                    &JsValue::from_str(&set.address),
188                                    &js_value,
189                                );
190                            }
191                        }
192                        Message::Snapshot(snapshot) => {
193                            for param in &snapshot.params {
194                                let js_value = value_to_js(&param.value);
195                                params_msg
196                                    .borrow_mut()
197                                    .insert(param.address.clone(), js_value.clone());
198
199                                if let Some(callback) = on_message_msg.borrow().as_ref() {
200                                    let _ = callback.call2(
201                                        &JsValue::NULL,
202                                        &JsValue::from_str(&param.address),
203                                        &js_value,
204                                    );
205                                }
206                            }
207                        }
208                        Message::Publish(pub_msg) => {
209                            let value = pub_msg
210                                .value
211                                .as_ref()
212                                .or(pub_msg.payload.as_ref())
213                                .map(value_to_js)
214                                .unwrap_or(JsValue::NULL);
215
216                            if let Some(callback) = on_message_msg.borrow().as_ref() {
217                                let _ = callback.call2(
218                                    &JsValue::NULL,
219                                    &JsValue::from_str(&pub_msg.address),
220                                    &value,
221                                );
222                            }
223                        }
224                        _ => {}
225                    }
226                }
227            }
228        }) as Box<dyn FnMut(MessageEvent)>);
229        self.ws
230            .set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
231        onmessage.forget();
232
233        // onclose handler
234        let on_disconnect_close = self.on_disconnect.clone();
235        let connected_close = connected.clone();
236        let onclose = Closure::wrap(Box::new(move |e: CloseEvent| {
237            *connected_close.borrow_mut() = false;
238            if let Some(callback) = on_disconnect_close.borrow().as_ref() {
239                let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&e.reason()));
240            }
241        }) as Box<dyn FnMut(CloseEvent)>);
242        self.ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
243        onclose.forget();
244
245        // onerror handler
246        let on_error_err = self.on_error.clone();
247        let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
248            if let Some(callback) = on_error_err.borrow().as_ref() {
249                let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&e.message()));
250            }
251        }) as Box<dyn FnMut(ErrorEvent)>);
252        self.ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
253        onerror.forget();
254
255        Ok(())
256    }
257
258    /// Check if connected
259    #[wasm_bindgen(getter)]
260    pub fn connected(&self) -> bool {
261        *self.connected.borrow()
262    }
263
264    /// Get session ID
265    #[wasm_bindgen(getter)]
266    pub fn session_id(&self) -> Option<String> {
267        self.session_id.borrow().clone()
268    }
269
270    /// Set connection callback
271    pub fn set_on_connect(&self, callback: js_sys::Function) {
272        *self.on_connect.borrow_mut() = Some(callback);
273    }
274
275    /// Set disconnect callback
276    pub fn set_on_disconnect(&self, callback: js_sys::Function) {
277        *self.on_disconnect.borrow_mut() = Some(callback);
278    }
279
280    /// Set message callback
281    pub fn set_on_message(&self, callback: js_sys::Function) {
282        *self.on_message.borrow_mut() = Some(callback);
283    }
284
285    /// Set error callback
286    pub fn set_on_error(&self, callback: js_sys::Function) {
287        *self.on_error.borrow_mut() = Some(callback);
288    }
289
290    /// Set authentication error callback
291    ///
292    /// Called when the server returns an authentication error (code 300 or 302).
293    /// The callback receives an object with `code` and `message` properties.
294    #[wasm_bindgen(js_name = setOnAuthError)]
295    pub fn set_on_auth_error(&self, callback: js_sys::Function) {
296        *self.on_auth_error.borrow_mut() = Some(callback);
297    }
298
299    /// Subscribe to address pattern
300    pub fn subscribe(&self, pattern: &str) -> u32 {
301        let id = {
302            let mut sub_id = self.sub_id.borrow_mut();
303            let id = *sub_id;
304            *sub_id += 1;
305            id
306        };
307
308        let msg = Message::Subscribe(SubscribeMessage {
309            id,
310            pattern: pattern.to_string(),
311            types: vec![],
312            options: Some(SubscribeOptions::default()),
313        });
314
315        self.send_message(&msg);
316        id
317    }
318
319    /// Unsubscribe
320    pub fn unsubscribe(&self, id: u32) {
321        let msg = Message::Unsubscribe(clasp_core::UnsubscribeMessage { id });
322        self.send_message(&msg);
323    }
324
325    /// Set a value
326    pub fn set(&self, address: &str, value: JsValue) {
327        let sf_value = js_to_value(&value);
328        let msg = Message::Set(SetMessage {
329            address: address.to_string(),
330            value: sf_value,
331            revision: None,
332            lock: false,
333            unlock: false,
334            ttl: None,
335        });
336        self.send_message(&msg);
337    }
338
339    /// Emit an event
340    pub fn emit(&self, address: &str, payload: JsValue) {
341        let sf_value = js_to_value(&payload);
342        let msg = Message::Publish(clasp_core::PublishMessage {
343            address: address.to_string(),
344            signal: Some(clasp_core::SignalType::Event),
345            value: None,
346            payload: Some(sf_value),
347            samples: None,
348            rate: None,
349            id: None,
350            phase: None,
351            timestamp: None,
352            timeline: None,
353        });
354        self.send_message(&msg);
355    }
356
357    /// Get cached value
358    pub fn get(&self, address: &str) -> JsValue {
359        self.params
360            .borrow()
361            .get(address)
362            .cloned()
363            .unwrap_or(JsValue::NULL)
364    }
365
366    /// Close connection
367    pub fn close(&self) {
368        let _ = self.ws.close();
369    }
370
371    /// Send a message
372    fn send_message(&self, msg: &Message) {
373        if let Ok(bytes) = codec::encode(msg) {
374            let array = js_sys::Uint8Array::from(bytes.as_ref());
375            let _ = self.ws.send_with_array_buffer(&array.buffer());
376        }
377    }
378}
379
380/// Convert Clasp Value to JsValue
381fn value_to_js(value: &Value) -> JsValue {
382    match value {
383        Value::Null => JsValue::NULL,
384        Value::Bool(b) => JsValue::from_bool(*b),
385        Value::Int(i) => JsValue::from_f64(*i as f64),
386        Value::Float(f) => JsValue::from_f64(*f),
387        Value::String(s) => JsValue::from_str(s),
388        Value::Bytes(b) => {
389            let array = js_sys::Uint8Array::from(b.as_slice());
390            array.into()
391        }
392        Value::Array(arr) => {
393            let js_arr = js_sys::Array::new();
394            for v in arr {
395                js_arr.push(&value_to_js(v));
396            }
397            js_arr.into()
398        }
399        Value::Map(map) => {
400            let obj = js_sys::Object::new();
401            for (k, v) in map {
402                js_sys::Reflect::set(&obj, &JsValue::from_str(k), &value_to_js(v)).unwrap();
403            }
404            obj.into()
405        }
406    }
407}
408
409/// Convert JsValue to Clasp Value
410fn js_to_value(js: &JsValue) -> Value {
411    if js.is_null() || js.is_undefined() {
412        Value::Null
413    } else if let Some(b) = js.as_bool() {
414        Value::Bool(b)
415    } else if let Some(f) = js.as_f64() {
416        if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
417            Value::Int(f as i64)
418        } else {
419            Value::Float(f)
420        }
421    } else if let Some(s) = js.as_string() {
422        Value::String(s)
423    } else if js_sys::Array::is_array(js) {
424        let arr: js_sys::Array = js.clone().into();
425        let values: Vec<Value> = arr.iter().map(|v| js_to_value(&v)).collect();
426        Value::Array(values)
427    } else if js.is_object() {
428        let obj: js_sys::Object = js.clone().into();
429        let mut map = HashMap::new();
430        let keys = js_sys::Object::keys(&obj);
431        for key in keys.iter() {
432            if let Some(k) = key.as_string() {
433                if let Ok(v) = js_sys::Reflect::get(&obj, &key) {
434                    map.insert(k, js_to_value(&v));
435                }
436            }
437        }
438        Value::Map(map)
439    } else {
440        Value::Null
441    }
442}