use automerge as am; use automerge::transaction::Transactable; use automerge::{Change, ChangeHash, Prop}; use js_sys::{Array, Object, Reflect, Uint8Array}; use std::collections::HashSet; use std::fmt::Display; use unicode_segmentation::UnicodeSegmentation; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; use crate::{ObjId, ScalarValue, Value}; pub(crate) struct JS(pub JsValue); pub(crate) struct AR(pub Array); impl From for JsValue { fn from(ar: AR) -> Self { ar.0.into() } } impl From for JsValue { fn from(js: JS) -> Self { js.0 } } impl From for JS { fn from(state: am::SyncState) -> Self { let shared_heads: JS = state.shared_heads.into(); let last_sent_heads: JS = state.last_sent_heads.into(); let their_heads: JS = state.their_heads.into(); let their_need: JS = state.their_need.into(); let sent_hashes: JS = state.sent_hashes.into(); let their_have = if let Some(have) = &state.their_have { JsValue::from(AR::from(have.as_slice()).0) } else { JsValue::null() }; let result: JsValue = Object::new().into(); // we can unwrap here b/c we made the object and know its not frozen Reflect::set(&result, &"sharedHeads".into(), &shared_heads.0).unwrap(); Reflect::set(&result, &"lastSentHeads".into(), &last_sent_heads.0).unwrap(); Reflect::set(&result, &"theirHeads".into(), &their_heads.0).unwrap(); Reflect::set(&result, &"theirNeed".into(), &their_need.0).unwrap(); Reflect::set(&result, &"theirHave".into(), &their_have).unwrap(); Reflect::set(&result, &"sentHashes".into(), &sent_hashes.0).unwrap(); JS(result) } } impl From> for JS { fn from(heads: Vec) -> Self { let heads: Array = heads .iter() .map(|h| JsValue::from_str(&h.to_string())) .collect(); JS(heads.into()) } } impl From> for JS { fn from(heads: HashSet) -> Self { let result: JsValue = Object::new().into(); for key in &heads { Reflect::set(&result, &key.to_string().into(), &true.into()).unwrap(); } JS(result) } } impl From>> for JS { fn from(heads: Option>) -> Self { if let Some(v) = heads { let v: Array = v .iter() .map(|h| JsValue::from_str(&h.to_string())) .collect(); JS(v.into()) } else { JS(JsValue::null()) } } } impl TryFrom for HashSet { type Error = JsValue; fn try_from(value: JS) -> Result { let mut result = HashSet::new(); for key in Reflect::own_keys(&value.0)?.iter() { if let Some(true) = Reflect::get(&value.0, &key)?.as_bool() { result.insert(key.into_serde().map_err(to_js_err)?); } } Ok(result) } } impl TryFrom for Vec { type Error = JsValue; fn try_from(value: JS) -> Result { let value = value.0.dyn_into::()?; let value: Result, _> = value.iter().map(|j| j.into_serde()).collect(); let value = value.map_err(to_js_err)?; Ok(value) } } impl From for Option> { fn from(value: JS) -> Self { let value = value.0.dyn_into::().ok()?; let value: Result, _> = value.iter().map(|j| j.into_serde()).collect(); let value = value.ok()?; Some(value) } } impl TryFrom for Vec { type Error = JsValue; fn try_from(value: JS) -> Result { let value = value.0.dyn_into::()?; let changes: Result, _> = value.iter().map(|j| j.dyn_into()).collect(); let changes = changes?; let changes: Result, _> = changes .iter() .map(|a| am::decode_change(a.to_vec())) .collect(); let changes = changes.map_err(to_js_err)?; Ok(changes) } } impl TryFrom for am::SyncState { type Error = JsValue; fn try_from(value: JS) -> Result { let value = value.0; let shared_heads = js_get(&value, "sharedHeads")?.try_into()?; let last_sent_heads = js_get(&value, "lastSentHeads")?.try_into()?; let their_heads = js_get(&value, "theirHeads")?.into(); let their_need = js_get(&value, "theirNeed")?.into(); let their_have = js_get(&value, "theirHave")?.try_into()?; let sent_hashes = js_get(&value, "sentHashes")?.try_into()?; Ok(am::SyncState { shared_heads, last_sent_heads, their_heads, their_need, their_have, sent_hashes, }) } } impl TryFrom for Option> { type Error = JsValue; fn try_from(value: JS) -> Result { if value.0.is_null() { Ok(None) } else { Ok(Some(value.try_into()?)) } } } impl TryFrom for Vec { type Error = JsValue; fn try_from(value: JS) -> Result { let value = value.0.dyn_into::()?; let have: Result, JsValue> = value .iter() .map(|s| { let last_sync = js_get(&s, "lastSync")?.try_into()?; let bloom = js_get(&s, "bloom")?.try_into()?; Ok(am::SyncHave { last_sync, bloom }) }) .collect(); let have = have?; Ok(have) } } impl TryFrom for am::BloomFilter { type Error = JsValue; fn try_from(value: JS) -> Result { let value: Uint8Array = value.0.dyn_into()?; let value = value.to_vec(); let value = value.as_slice().try_into().map_err(to_js_err)?; Ok(value) } } impl From<&[ChangeHash]> for AR { fn from(value: &[ChangeHash]) -> Self { AR(value .iter() .map(|h| JsValue::from_str(&hex::encode(&h.0))) .collect()) } } impl From<&[Change]> for AR { fn from(value: &[Change]) -> Self { let changes: Array = value .iter() .map(|c| Uint8Array::from(c.raw_bytes())) .collect(); AR(changes) } } impl From<&[am::SyncHave]> for AR { fn from(value: &[am::SyncHave]) -> Self { AR(value .iter() .map(|have| { let last_sync: Array = have .last_sync .iter() .map(|h| JsValue::from_str(&hex::encode(&h.0))) .collect(); // FIXME - the clone and the unwrap here shouldnt be needed - look at into_bytes() let bloom = Uint8Array::from(have.bloom.clone().into_bytes().unwrap().as_slice()); let obj: JsValue = Object::new().into(); // we can unwrap here b/c we created the object and know its not frozen Reflect::set(&obj, &"lastSync".into(), &last_sync.into()).unwrap(); Reflect::set(&obj, &"bloom".into(), &bloom.into()).unwrap(); obj }) .collect()) } } pub(crate) fn to_js_err(err: T) -> JsValue { js_sys::Error::new(&std::format!("{}", err)).into() } pub(crate) fn js_get>(obj: J, prop: &str) -> Result { Ok(JS(Reflect::get(&obj.into(), &prop.into())?)) } pub(crate) fn js_set>(obj: &JsValue, prop: &str, val: V) -> Result { Reflect::set(obj, &prop.into(), &val.into()) } pub(crate) fn to_prop(p: JsValue) -> Result { if let Some(s) = p.as_string() { Ok(Prop::Map(s)) } else if let Some(n) = p.as_f64() { Ok(Prop::Seq(n as usize)) } else { Err(to_js_err("prop must me a string or number")) } } pub(crate) fn to_objtype( value: &JsValue, datatype: &Option, ) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> { match datatype.as_deref() { Some("map") => { let map = value.clone().dyn_into::().ok()?; // FIXME unwrap let map = js_sys::Object::keys(&map) .iter() .zip(js_sys::Object::values(&map).iter()) .map(|(key, val)| (key.as_string().unwrap().into(), val)) .collect(); Some((am::ObjType::Map, map)) } Some("list") => { let list = value.clone().dyn_into::().ok()?; let list = list .iter() .enumerate() .map(|(i, e)| (i.into(), e)) .collect(); Some((am::ObjType::List, list)) } Some("text") => { let text = value.as_string()?; let text = text .graphemes(true) .enumerate() .map(|(i, ch)| (i.into(), ch.into())) .collect(); Some((am::ObjType::Text, text)) } Some(_) => None, None => { if let Ok(list) = value.clone().dyn_into::() { let list = list .iter() .enumerate() .map(|(i, e)| (i.into(), e)) .collect(); Some((am::ObjType::List, list)) } else if let Ok(map) = value.clone().dyn_into::() { // FIXME unwrap let map = js_sys::Object::keys(&map) .iter() .zip(js_sys::Object::values(&map).iter()) .map(|(key, val)| (key.as_string().unwrap().into(), val)) .collect(); Some((am::ObjType::Map, map)) } else if let Some(text) = value.as_string() { let text = text .graphemes(true) .enumerate() .map(|(i, ch)| (i.into(), ch.into())) .collect(); Some((am::ObjType::Text, text)) } else { None } } } } pub(crate) fn get_heads(heads: Option) -> Option> { let heads = heads?; let heads: Result, _> = heads.iter().map(|j| j.into_serde()).collect(); heads.ok() } pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { let keys = doc.keys(obj); let map = Object::new(); for k in keys { let val = doc.value(obj, &k); match val { Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Map || o == am::ObjType::Table => { Reflect::set(&map, &k.into(), &map_to_js(doc, &exid)).unwrap(); } Ok(Some((Value::Object(_), exid))) => { Reflect::set(&map, &k.into(), &list_to_js(doc, &exid)).unwrap(); } Ok(Some((Value::Scalar(v), _))) => { Reflect::set(&map, &k.into(), &ScalarValue(v).into()).unwrap(); } _ => (), }; } map.into() } fn list_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { let len = doc.length(obj); let array = Array::new(); for i in 0..len { let val = doc.value(obj, i as usize); match val { Ok(Some((Value::Object(o), exid))) if o == am::ObjType::Map || o == am::ObjType::Table => { array.push(&map_to_js(doc, &exid)); } Ok(Some((Value::Object(_), exid))) => { array.push(&list_to_js(doc, &exid)); } Ok(Some((Value::Scalar(v), _))) => { array.push(&ScalarValue(v).into()); } _ => (), }; } array.into() }