use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
use clasp_core::{
codec, HelloMessage, Message, SetMessage, SubscribeMessage, SubscribeOptions,
Value, PROTOCOL_VERSION, WS_SUBPROTOCOL,
};
#[cfg(feature = "console_error_panic_hook")]
pub fn set_panic_hook() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen(start)]
pub fn init() {
#[cfg(feature = "console_error_panic_hook")]
set_panic_hook();
}
#[wasm_bindgen]
pub struct ClaspWasm {
ws: WebSocket,
session_id: Rc<RefCell<Option<String>>>,
connected: Rc<RefCell<bool>>,
params: Rc<RefCell<HashMap<String, JsValue>>>,
on_message: Rc<RefCell<Option<js_sys::Function>>>,
on_connect: Rc<RefCell<Option<js_sys::Function>>>,
on_disconnect: Rc<RefCell<Option<js_sys::Function>>>,
on_error: Rc<RefCell<Option<js_sys::Function>>>,
on_auth_error: Rc<RefCell<Option<js_sys::Function>>>,
sub_id: Rc<RefCell<u32>>,
token: Rc<RefCell<Option<String>>>,
}
#[wasm_bindgen]
impl ClaspWasm {
#[wasm_bindgen(constructor)]
pub fn new(url: &str) -> Result<ClaspWasm, JsValue> {
Self::new_with_token(url, None)
}
#[wasm_bindgen(js_name = newWithToken)]
pub fn new_with_token(url: &str, token: Option<String>) -> Result<ClaspWasm, JsValue> {
let ws = WebSocket::new_with_str(url, WS_SUBPROTOCOL)?;
ws.set_binary_type(web_sys::BinaryType::Arraybuffer);
let client = ClaspWasm {
ws,
session_id: Rc::new(RefCell::new(None)),
connected: Rc::new(RefCell::new(false)),
params: Rc::new(RefCell::new(HashMap::new())),
on_message: Rc::new(RefCell::new(None)),
on_connect: Rc::new(RefCell::new(None)),
on_disconnect: Rc::new(RefCell::new(None)),
on_error: Rc::new(RefCell::new(None)),
on_auth_error: Rc::new(RefCell::new(None)),
sub_id: Rc::new(RefCell::new(1)),
token: Rc::new(RefCell::new(token)),
};
client.setup_handlers()?;
Ok(client)
}
#[wasm_bindgen(js_name = setToken)]
pub fn set_token(&self, token: String) {
*self.token.borrow_mut() = Some(token);
}
fn setup_handlers(&self) -> Result<(), JsValue> {
let connected = self.connected.clone();
let session_id = self.session_id.clone();
let params = self.params.clone();
let on_connect = self.on_connect.clone();
let on_message = self.on_message.clone();
let on_auth_error = self.on_auth_error.clone();
let ws = self.ws.clone();
let token = self.token.clone();
let ws_open = ws.clone();
let token_open = token.clone();
let onopen = Closure::wrap(Box::new(move |_: JsValue| {
let token_value = token_open.borrow().clone();
let hello = Message::Hello(HelloMessage {
version: PROTOCOL_VERSION,
name: "Clasp WASM Client".to_string(),
features: vec![
"param".to_string(),
"event".to_string(),
"stream".to_string(),
],
capabilities: None,
token: token_value,
});
if let Ok(bytes) = codec::encode(&hello) {
let array = js_sys::Uint8Array::from(bytes.as_ref());
let _ = ws_open.send_with_array_buffer(&array.buffer());
}
}) as Box<dyn FnMut(JsValue)>);
self.ws.set_onopen(Some(onopen.as_ref().unchecked_ref()));
onopen.forget();
let connected_msg = connected.clone();
let session_msg = session_id.clone();
let params_msg = params.clone();
let on_connect_msg = on_connect.clone();
let on_message_msg = on_message.clone();
let on_auth_error_msg = on_auth_error.clone();
let ws_msg = ws.clone();
let onmessage = Closure::wrap(Box::new(move |e: MessageEvent| {
if let Ok(abuf) = e.data().dyn_into::<js_sys::ArrayBuffer>() {
let array = js_sys::Uint8Array::new(&abuf);
let bytes: Vec<u8> = array.to_vec();
if let Ok((msg, _)) = codec::decode(&bytes) {
match &msg {
Message::Welcome(welcome) => {
*session_msg.borrow_mut() = Some(welcome.session.clone());
*connected_msg.borrow_mut() = true;
if let Some(callback) = on_connect_msg.borrow().as_ref() {
let _ = callback.call0(&JsValue::NULL);
}
}
Message::Error(error) => {
match error.code {
300 | 302 => {
let _ = ws_msg.close();
*connected_msg.borrow_mut() = false;
if let Some(callback) = on_auth_error_msg.borrow().as_ref() {
let error_obj = js_sys::Object::new();
let _ = js_sys::Reflect::set(
&error_obj,
&JsValue::from_str("code"),
&JsValue::from_f64(error.code as f64),
);
let _ = js_sys::Reflect::set(
&error_obj,
&JsValue::from_str("message"),
&JsValue::from_str(&error.message),
);
let _ = callback.call1(&JsValue::NULL, &error_obj.into());
}
}
_ => {
}
}
}
Message::Set(set) => {
let js_value = value_to_js(&set.value);
params_msg
.borrow_mut()
.insert(set.address.clone(), js_value.clone());
if let Some(callback) = on_message_msg.borrow().as_ref() {
let _ = callback.call2(
&JsValue::NULL,
&JsValue::from_str(&set.address),
&js_value,
);
}
}
Message::Snapshot(snapshot) => {
for param in &snapshot.params {
let js_value = value_to_js(¶m.value);
params_msg
.borrow_mut()
.insert(param.address.clone(), js_value.clone());
if let Some(callback) = on_message_msg.borrow().as_ref() {
let _ = callback.call2(
&JsValue::NULL,
&JsValue::from_str(¶m.address),
&js_value,
);
}
}
}
Message::Publish(pub_msg) => {
let value = pub_msg
.value
.as_ref()
.or(pub_msg.payload.as_ref())
.map(value_to_js)
.unwrap_or(JsValue::NULL);
if let Some(callback) = on_message_msg.borrow().as_ref() {
let _ = callback.call2(
&JsValue::NULL,
&JsValue::from_str(&pub_msg.address),
&value,
);
}
}
_ => {}
}
}
}
}) as Box<dyn FnMut(MessageEvent)>);
self.ws
.set_onmessage(Some(onmessage.as_ref().unchecked_ref()));
onmessage.forget();
let on_disconnect_close = self.on_disconnect.clone();
let connected_close = connected.clone();
let onclose = Closure::wrap(Box::new(move |e: CloseEvent| {
*connected_close.borrow_mut() = false;
if let Some(callback) = on_disconnect_close.borrow().as_ref() {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&e.reason()));
}
}) as Box<dyn FnMut(CloseEvent)>);
self.ws.set_onclose(Some(onclose.as_ref().unchecked_ref()));
onclose.forget();
let on_error_err = self.on_error.clone();
let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
if let Some(callback) = on_error_err.borrow().as_ref() {
let _ = callback.call1(&JsValue::NULL, &JsValue::from_str(&e.message()));
}
}) as Box<dyn FnMut(ErrorEvent)>);
self.ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
onerror.forget();
Ok(())
}
#[wasm_bindgen(getter)]
pub fn connected(&self) -> bool {
*self.connected.borrow()
}
#[wasm_bindgen(getter)]
pub fn session_id(&self) -> Option<String> {
self.session_id.borrow().clone()
}
pub fn set_on_connect(&self, callback: js_sys::Function) {
*self.on_connect.borrow_mut() = Some(callback);
}
pub fn set_on_disconnect(&self, callback: js_sys::Function) {
*self.on_disconnect.borrow_mut() = Some(callback);
}
pub fn set_on_message(&self, callback: js_sys::Function) {
*self.on_message.borrow_mut() = Some(callback);
}
pub fn set_on_error(&self, callback: js_sys::Function) {
*self.on_error.borrow_mut() = Some(callback);
}
#[wasm_bindgen(js_name = setOnAuthError)]
pub fn set_on_auth_error(&self, callback: js_sys::Function) {
*self.on_auth_error.borrow_mut() = Some(callback);
}
pub fn subscribe(&self, pattern: &str) -> u32 {
let id = {
let mut sub_id = self.sub_id.borrow_mut();
let id = *sub_id;
*sub_id += 1;
id
};
let msg = Message::Subscribe(SubscribeMessage {
id,
pattern: pattern.to_string(),
types: vec![],
options: Some(SubscribeOptions::default()),
});
self.send_message(&msg);
id
}
pub fn unsubscribe(&self, id: u32) {
let msg = Message::Unsubscribe(clasp_core::UnsubscribeMessage { id });
self.send_message(&msg);
}
pub fn set(&self, address: &str, value: JsValue) {
let sf_value = js_to_value(&value);
let msg = Message::Set(SetMessage {
address: address.to_string(),
value: sf_value,
revision: None,
lock: false,
unlock: false,
});
self.send_message(&msg);
}
pub fn emit(&self, address: &str, payload: JsValue) {
let sf_value = js_to_value(&payload);
let msg = Message::Publish(clasp_core::PublishMessage {
address: address.to_string(),
signal: Some(clasp_core::SignalType::Event),
value: None,
payload: Some(sf_value),
samples: None,
rate: None,
id: None,
phase: None,
timestamp: None,
});
self.send_message(&msg);
}
pub fn get(&self, address: &str) -> JsValue {
self.params
.borrow()
.get(address)
.cloned()
.unwrap_or(JsValue::NULL)
}
pub fn close(&self) {
let _ = self.ws.close();
}
fn send_message(&self, msg: &Message) {
if let Ok(bytes) = codec::encode(msg) {
let array = js_sys::Uint8Array::from(bytes.as_ref());
let _ = self.ws.send_with_array_buffer(&array.buffer());
}
}
}
fn value_to_js(value: &Value) -> JsValue {
match value {
Value::Null => JsValue::NULL,
Value::Bool(b) => JsValue::from_bool(*b),
Value::Int(i) => JsValue::from_f64(*i as f64),
Value::Float(f) => JsValue::from_f64(*f),
Value::String(s) => JsValue::from_str(s),
Value::Bytes(b) => {
let array = js_sys::Uint8Array::from(b.as_slice());
array.into()
}
Value::Array(arr) => {
let js_arr = js_sys::Array::new();
for v in arr {
js_arr.push(&value_to_js(v));
}
js_arr.into()
}
Value::Map(map) => {
let obj = js_sys::Object::new();
for (k, v) in map {
js_sys::Reflect::set(&obj, &JsValue::from_str(k), &value_to_js(v)).unwrap();
}
obj.into()
}
}
}
fn js_to_value(js: &JsValue) -> Value {
if js.is_null() || js.is_undefined() {
Value::Null
} else if let Some(b) = js.as_bool() {
Value::Bool(b)
} else if let Some(f) = js.as_f64() {
if f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
Value::Int(f as i64)
} else {
Value::Float(f)
}
} else if let Some(s) = js.as_string() {
Value::String(s)
} else if js_sys::Array::is_array(js) {
let arr: js_sys::Array = js.clone().into();
let values: Vec<Value> = arr.iter().map(|v| js_to_value(&v)).collect();
Value::Array(values)
} else if js.is_object() {
let obj: js_sys::Object = js.clone().into();
let mut map = HashMap::new();
let keys = js_sys::Object::keys(&obj);
for key in keys.iter() {
if let Some(k) = key.as_string() {
if let Ok(v) = js_sys::Reflect::get(&obj, &key) {
map.insert(k, js_to_value(&v));
}
}
}
Value::Map(map)
} else {
Value::Null
}
}