1#[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#[wasm_bindgen(start)]
27pub fn init() {
28 #[cfg(feature = "console_error_panic_hook")]
29 set_panic_hook();
30}
31
32#[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 #[wasm_bindgen(constructor)]
52 pub fn new(url: &str) -> Result<ClaspWasm, JsValue> {
53 Self::new_with_token(url, None)
54 }
55
56 #[wasm_bindgen(js_name = newWithToken)]
58 pub fn new_with_token(url: &str, token: Option<String>) -> Result<ClaspWasm, JsValue> {
59 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 #[wasm_bindgen(js_name = setToken)]
84 pub fn set_token(&self, token: String) {
85 *self.token.borrow_mut() = Some(token);
86 }
87
88 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 let ws_open = ws.clone();
101 let token_open = token.clone();
102 let onopen = Closure::wrap(Box::new(move |_: JsValue| {
103 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 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 match error.code {
152 300 | 302 => {
153 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 }
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(¶m.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(¶m.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 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 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 #[wasm_bindgen(getter)]
260 pub fn connected(&self) -> bool {
261 *self.connected.borrow()
262 }
263
264 #[wasm_bindgen(getter)]
266 pub fn session_id(&self) -> Option<String> {
267 self.session_id.borrow().clone()
268 }
269
270 pub fn set_on_connect(&self, callback: js_sys::Function) {
272 *self.on_connect.borrow_mut() = Some(callback);
273 }
274
275 pub fn set_on_disconnect(&self, callback: js_sys::Function) {
277 *self.on_disconnect.borrow_mut() = Some(callback);
278 }
279
280 pub fn set_on_message(&self, callback: js_sys::Function) {
282 *self.on_message.borrow_mut() = Some(callback);
283 }
284
285 pub fn set_on_error(&self, callback: js_sys::Function) {
287 *self.on_error.borrow_mut() = Some(callback);
288 }
289
290 #[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 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 pub fn unsubscribe(&self, id: u32) {
321 let msg = Message::Unsubscribe(clasp_core::UnsubscribeMessage { id });
322 self.send_message(&msg);
323 }
324
325 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 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 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 pub fn close(&self) {
368 let _ = self.ws.close();
369 }
370
371 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
380fn 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
409fn 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}