#![doc( html_logo_url = "https://raw.githubusercontent.com/automerge/automerge-rs/main/img/brandmark.svg", html_favicon_url = "https:///raw.githubusercontent.com/automerge/automerge-rs/main/img/favicon.ico" )] #![warn( missing_debug_implementations, // missing_docs, // TODO: add documentation! rust_2021_compatibility, rust_2018_idioms, unreachable_pub, bad_style, const_err, dead_code, improper_ctypes, non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements, patterns_in_fns_without_body, private_in_public, unconditional_recursion, unused, unused_allocation, unused_comparisons, unused_parens, while_true )] #![allow(clippy::unused_unit)] use am::transaction::CommitOptions; use am::transaction::Transactable; use automerge as am; use automerge::{Change, ObjId, Prop, Value, ROOT}; use js_sys::{Array, Object, Uint8Array}; use std::convert::TryInto; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; mod interop; mod observer; mod sync; mod value; use observer::Observer; use interop::{ apply_patch, get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, to_js_err, to_objtype, to_prop, AR, JS, }; use sync::SyncState; use value::{datatype, ScalarValue}; #[allow(unused_macros)] macro_rules! log { ( $( $t:tt )* ) => { web_sys::console::log_1(&format!( $( $t )* ).into()); }; } type AutoCommit = am::AutoCommitWithObs; #[cfg(feature = "wee_alloc")] #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[wasm_bindgen] #[derive(Debug)] pub struct Automerge { doc: AutoCommit, } #[wasm_bindgen] impl Automerge { pub fn new(actor: Option) -> Result { let mut doc = AutoCommit::default(); if let Some(a) = actor { let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec()); doc.set_actor(a); } Ok(Automerge { doc }) } #[allow(clippy::should_implement_trait)] pub fn clone(&mut self, actor: Option) -> Result { let mut automerge = Automerge { doc: self.doc.clone(), }; if let Some(s) = actor { let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); automerge.doc.set_actor(actor); } Ok(automerge) } pub fn fork(&mut self, actor: Option) -> Result { let mut automerge = Automerge { doc: self.doc.fork(), }; if let Some(s) = actor { let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); automerge.doc.set_actor(actor); } Ok(automerge) } #[wasm_bindgen(js_name = forkAt)] pub fn fork_at(&mut self, heads: JsValue, actor: Option) -> Result { let deps: Vec<_> = JS(heads).try_into()?; let mut automerge = Automerge { doc: self.doc.fork_at(&deps)?, }; if let Some(s) = actor { let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); automerge.doc.set_actor(actor); } Ok(automerge) } pub fn free(self) {} #[wasm_bindgen(js_name = pendingOps)] pub fn pending_ops(&self) -> JsValue { (self.doc.pending_ops() as u32).into() } pub fn commit(&mut self, message: Option, time: Option) -> JsValue { let mut commit_opts = CommitOptions::default(); if let Some(message) = message { commit_opts.set_message(message); } if let Some(time) = time { commit_opts.set_time(time as i64); } let hash = self.doc.commit_with(commit_opts); JsValue::from_str(&hex::encode(&hash.0)) } pub fn merge(&mut self, other: &mut Automerge) -> Result { let heads = self.doc.merge(&mut other.doc)?; let heads: Array = heads .iter() .map(|h| JsValue::from_str(&hex::encode(&h.0))) .collect(); Ok(heads) } pub fn rollback(&mut self) -> f64 { self.doc.rollback() as f64 } pub fn keys(&self, obj: JsValue, heads: Option) -> Result { let obj = self.import(obj)?; let result = if let Some(heads) = get_heads(heads) { self.doc .keys_at(&obj, &heads) .map(|s| JsValue::from_str(&s)) .collect() } else { self.doc.keys(&obj).map(|s| JsValue::from_str(&s)).collect() }; Ok(result) } pub fn text(&self, obj: JsValue, heads: Option) -> Result { let obj = self.import(obj)?; if let Some(heads) = get_heads(heads) { Ok(self.doc.text_at(&obj, &heads)?) } else { Ok(self.doc.text(&obj)?) } } pub fn splice( &mut self, obj: JsValue, start: f64, delete_count: f64, text: JsValue, ) -> Result<(), JsValue> { let obj = self.import(obj)?; let start = start as usize; let delete_count = delete_count as usize; let mut vals = vec![]; if let Some(t) = text.as_string() { self.doc.splice_text(&obj, start, delete_count, &t)?; } else { if let Ok(array) = text.dyn_into::() { for i in array.iter() { let value = self .import_scalar(&i, &None) .ok_or_else(|| to_js_err("expected scalar"))?; vals.push(value); } } self.doc .splice(&obj, start, delete_count, vals.into_iter())?; } Ok(()) } pub fn push(&mut self, obj: JsValue, value: JsValue, datatype: JsValue) -> Result<(), JsValue> { let obj = self.import(obj)?; let value = self .import_scalar(&value, &datatype.as_string()) .ok_or_else(|| to_js_err("invalid scalar value"))?; let index = self.doc.length(&obj); self.doc.insert(&obj, index, value)?; Ok(()) } #[wasm_bindgen(js_name = pushObject)] pub fn push_object(&mut self, obj: JsValue, value: JsValue) -> Result, JsValue> { let obj = self.import(obj)?; let (value, subvals) = to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let index = self.doc.length(&obj); let opid = self.doc.insert_object(&obj, index, value)?; self.subset(&opid, subvals)?; Ok(opid.to_string().into()) } pub fn insert( &mut self, obj: JsValue, index: f64, value: JsValue, datatype: JsValue, ) -> Result<(), JsValue> { let obj = self.import(obj)?; let index = index as f64; let value = self .import_scalar(&value, &datatype.as_string()) .ok_or_else(|| to_js_err("expected scalar value"))?; self.doc.insert(&obj, index as usize, value)?; Ok(()) } #[wasm_bindgen(js_name = insertObject)] pub fn insert_object( &mut self, obj: JsValue, index: f64, value: JsValue, ) -> Result, JsValue> { let obj = self.import(obj)?; let index = index as f64; let (value, subvals) = to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let opid = self.doc.insert_object(&obj, index as usize, value)?; self.subset(&opid, subvals)?; Ok(opid.to_string().into()) } pub fn put( &mut self, obj: JsValue, prop: JsValue, value: JsValue, datatype: JsValue, ) -> Result<(), JsValue> { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; let value = self .import_scalar(&value, &datatype.as_string()) .ok_or_else(|| to_js_err("expected scalar value"))?; self.doc.put(&obj, prop, value)?; Ok(()) } #[wasm_bindgen(js_name = putObject)] pub fn put_object( &mut self, obj: JsValue, prop: JsValue, value: JsValue, ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; let (value, subvals) = to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let opid = self.doc.put_object(&obj, prop, value)?; self.subset(&opid, subvals)?; Ok(opid.to_string().into()) } fn subset(&mut self, obj: &am::ObjId, vals: Vec<(am::Prop, JsValue)>) -> Result<(), JsValue> { for (p, v) in vals { let (value, subvals) = self.import_value(&v, None)?; //let opid = self.0.set(id, p, value)?; let opid = match (p, value) { (Prop::Map(s), Value::Object(objtype)) => { Some(self.doc.put_object(obj, s, objtype)?) } (Prop::Map(s), Value::Scalar(scalar)) => { self.doc.put(obj, s, scalar.into_owned())?; None } (Prop::Seq(i), Value::Object(objtype)) => { Some(self.doc.insert_object(obj, i, objtype)?) } (Prop::Seq(i), Value::Scalar(scalar)) => { self.doc.insert(obj, i, scalar.into_owned())?; None } }; if let Some(opid) = opid { self.subset(&opid, subvals)?; } } Ok(()) } pub fn increment( &mut self, obj: JsValue, prop: JsValue, value: JsValue, ) -> Result<(), JsValue> { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; let value: f64 = value .as_f64() .ok_or_else(|| to_js_err("increment needs a numeric value"))?; self.doc.increment(&obj, prop, value as i64)?; Ok(()) } #[wasm_bindgen(js_name = get)] pub fn get( &self, obj: JsValue, prop: JsValue, heads: Option, ) -> Result { let obj = self.import(obj)?; let prop = to_prop(prop); let heads = get_heads(heads); if let Ok(prop) = prop { let value = if let Some(h) = heads { self.doc.get_at(&obj, prop, &h)? } else { self.doc.get(&obj, prop)? }; match value { Some((Value::Object(_), obj_id)) => Ok(obj_id.to_string().into()), Some((Value::Scalar(value), _)) => Ok(ScalarValue(value).into()), None => Ok(JsValue::undefined()), } } else { Ok(JsValue::undefined()) } } #[wasm_bindgen(js_name = getWithType)] pub fn get_with_type( &self, obj: JsValue, prop: JsValue, heads: Option, ) -> Result { let obj = self.import(obj)?; let result = Array::new(); let prop = to_prop(prop); let heads = get_heads(heads); if let Ok(prop) = prop { let value = if let Some(h) = heads { self.doc.get_at(&obj, prop, &h)? } else { self.doc.get(&obj, prop)? }; match value { Some((Value::Object(obj_type), obj_id)) => { result.push(&obj_type.to_string().into()); result.push(&obj_id.to_string().into()); Ok(result.into()) } Some((Value::Scalar(value), _)) => { result.push(&datatype(&value).into()); result.push(&ScalarValue(value).into()); Ok(result.into()) } None => Ok(JsValue::null()), } } else { Ok(JsValue::null()) } } #[wasm_bindgen(js_name = getAll)] pub fn get_all( &self, obj: JsValue, arg: JsValue, heads: Option, ) -> Result { let obj = self.import(obj)?; let result = Array::new(); let prop = to_prop(arg); if let Ok(prop) = prop { let values = if let Some(heads) = get_heads(heads) { self.doc.get_all_at(&obj, prop, &heads) } else { self.doc.get_all(&obj, prop) } .map_err(to_js_err)?; for value in values { match value { (Value::Object(obj_type), obj_id) => { let sub = Array::new(); sub.push(&obj_type.to_string().into()); sub.push(&obj_id.to_string().into()); result.push(&sub.into()); } (Value::Scalar(value), id) => { let sub = Array::new(); sub.push(&datatype(&value).into()); sub.push(&ScalarValue(value).into()); sub.push(&id.to_string().into()); result.push(&sub.into()); } } } } Ok(result) } #[wasm_bindgen(js_name = enablePatches)] pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> { let enable = enable .as_bool() .ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?; self.doc.observer().enable(enable); Ok(()) } #[wasm_bindgen(js_name = applyPatches)] pub fn apply_patches(&mut self, mut object: JsValue) -> Result { let patches = self.doc.observer().take_patches(); for p in patches { object = apply_patch(object, &p)?; } Ok(object) } #[wasm_bindgen(js_name = popPatches)] pub fn pop_patches(&mut self) -> Result { // transactions send out observer updates as they occur, not waiting for them to be // committed. // If we pop the patches then we won't be able to revert them. let patches = self.doc.observer().take_patches(); let result = Array::new(); for p in patches { result.push(&p.try_into()?); } Ok(result) } pub fn length(&self, obj: JsValue, heads: Option) -> Result { let obj = self.import(obj)?; if let Some(heads) = get_heads(heads) { Ok(self.doc.length_at(&obj, &heads) as f64) } else { Ok(self.doc.length(&obj) as f64) } } pub fn delete(&mut self, obj: JsValue, prop: JsValue) -> Result<(), JsValue> { let obj = self.import(obj)?; let prop = to_prop(prop)?; self.doc.delete(&obj, prop).map_err(to_js_err)?; Ok(()) } pub fn save(&mut self) -> Uint8Array { Uint8Array::from(self.doc.save().as_slice()) } #[wasm_bindgen(js_name = saveIncremental)] pub fn save_incremental(&mut self) -> Uint8Array { let bytes = self.doc.save_incremental(); Uint8Array::from(bytes.as_slice()) } #[wasm_bindgen(js_name = loadIncremental)] pub fn load_incremental(&mut self, data: Uint8Array) -> Result { let data = data.to_vec(); let len = self.doc.load_incremental(&data).map_err(to_js_err)?; Ok(len as f64) } #[wasm_bindgen(js_name = applyChanges)] pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> { let changes: Vec<_> = JS(changes).try_into()?; self.doc.apply_changes(changes).map_err(to_js_err)?; Ok(()) } #[wasm_bindgen(js_name = getChanges)] pub fn get_changes(&mut self, have_deps: JsValue) -> Result { let deps: Vec<_> = JS(have_deps).try_into()?; let changes = self.doc.get_changes(&deps)?; let changes: Array = changes .iter() .map(|c| Uint8Array::from(c.raw_bytes())) .collect(); Ok(changes) } #[wasm_bindgen(js_name = getChangeByHash)] pub fn get_change_by_hash(&mut self, hash: JsValue) -> Result { let hash = hash.into_serde().map_err(to_js_err)?; let change = self.doc.get_change_by_hash(&hash); if let Some(c) = change { Ok(Uint8Array::from(c.raw_bytes()).into()) } else { Ok(JsValue::null()) } } #[wasm_bindgen(js_name = getChangesAdded)] pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result { let changes = self.doc.get_changes_added(&mut other.doc); let changes: Array = changes .iter() .map(|c| Uint8Array::from(c.raw_bytes())) .collect(); Ok(changes) } #[wasm_bindgen(js_name = getHeads)] pub fn get_heads(&mut self) -> Array { let heads = self.doc.get_heads(); let heads: Array = heads .iter() .map(|h| JsValue::from_str(&hex::encode(&h.0))) .collect(); heads } #[wasm_bindgen(js_name = getActorId)] pub fn get_actor_id(&self) -> String { let actor = self.doc.get_actor(); actor.to_string() } #[wasm_bindgen(js_name = getLastLocalChange)] pub fn get_last_local_change(&mut self) -> Result { if let Some(change) = self.doc.get_last_local_change() { Ok(Uint8Array::from(change.raw_bytes()).into()) } else { Ok(JsValue::null()) } } pub fn dump(&mut self) { self.doc.dump() } #[wasm_bindgen(js_name = getMissingDeps)] pub fn get_missing_deps(&mut self, heads: Option) -> Result { let heads = get_heads(heads).unwrap_or_default(); let deps = self.doc.get_missing_deps(&heads); let deps: Array = deps .iter() .map(|h| JsValue::from_str(&hex::encode(&h.0))) .collect(); Ok(deps) } #[wasm_bindgen(js_name = receiveSyncMessage)] pub fn receive_sync_message( &mut self, state: &mut SyncState, message: Uint8Array, ) -> Result<(), JsValue> { let message = message.to_vec(); let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?; self.doc .receive_sync_message(&mut state.0, message) .map_err(to_js_err)?; Ok(()) } #[wasm_bindgen(js_name = generateSyncMessage)] pub fn generate_sync_message(&mut self, state: &mut SyncState) -> Result { if let Some(message) = self.doc.generate_sync_message(&mut state.0) { Ok(Uint8Array::from(message.encode().as_slice()).into()) } else { Ok(JsValue::null()) } } #[wasm_bindgen(js_name = toJS)] pub fn to_js(&self) -> JsValue { map_to_js(&self.doc, &ROOT) } pub fn materialize(&self, obj: JsValue, heads: Option) -> Result { let obj = self.import(obj).unwrap_or(ROOT); let heads = get_heads(heads); if let Some(heads) = heads { match self.doc.object_type(&obj) { Some(am::ObjType::Map) => Ok(map_to_js_at(&self.doc, &obj, heads.as_slice())), Some(am::ObjType::List) => Ok(list_to_js_at(&self.doc, &obj, heads.as_slice())), Some(am::ObjType::Text) => Ok(self.doc.text_at(&obj, heads.as_slice())?.into()), Some(am::ObjType::Table) => Ok(map_to_js_at(&self.doc, &obj, heads.as_slice())), None => Err(to_js_err(format!("invalid obj {}", obj))), } } else { match self.doc.object_type(&obj) { Some(am::ObjType::Map) => Ok(map_to_js(&self.doc, &obj)), Some(am::ObjType::List) => Ok(list_to_js(&self.doc, &obj)), Some(am::ObjType::Text) => Ok(self.doc.text(&obj)?.into()), Some(am::ObjType::Table) => Ok(map_to_js(&self.doc, &obj)), None => Err(to_js_err(format!("invalid obj {}", obj))), } } } fn import(&self, id: JsValue) -> Result { if let Some(s) = id.as_string() { if let Some(post) = s.strip_prefix('/') { let mut obj = ROOT; let mut is_map = true; let parts = post.split('/'); for prop in parts { if prop.is_empty() { break; } let val = if is_map { self.doc.get(obj, prop)? } else { self.doc.get(obj, am::Prop::Seq(prop.parse().unwrap()))? }; match val { Some((am::Value::Object(am::ObjType::Map), id)) => { is_map = true; obj = id; } Some((am::Value::Object(am::ObjType::Table), id)) => { is_map = true; obj = id; } Some((am::Value::Object(_), id)) => { is_map = false; obj = id; } None => return Err(to_js_err(format!("invalid path '{}'", s))), _ => return Err(to_js_err(format!("path '{}' is not an object", s))), }; } Ok(obj) } else { Ok(self.doc.import(&s)?) } } else { Err(to_js_err("invalid objid")) } } fn import_prop(&self, prop: JsValue) -> Result { if let Some(s) = prop.as_string() { Ok(s.into()) } else if let Some(n) = prop.as_f64() { Ok((n as usize).into()) } else { Err(to_js_err(format!("invalid prop {:?}", prop))) } } fn import_scalar(&self, value: &JsValue, datatype: &Option) -> Option { match datatype.as_deref() { Some("boolean") => value.as_bool().map(am::ScalarValue::Boolean), Some("int") => value.as_f64().map(|v| am::ScalarValue::Int(v as i64)), Some("uint") => value.as_f64().map(|v| am::ScalarValue::Uint(v as u64)), Some("str") => value.as_string().map(|v| am::ScalarValue::Str(v.into())), Some("f64") => value.as_f64().map(am::ScalarValue::F64), Some("bytes") => Some(am::ScalarValue::Bytes( value.clone().dyn_into::().unwrap().to_vec(), )), Some("counter") => value.as_f64().map(|v| am::ScalarValue::counter(v as i64)), Some("timestamp") => { if let Some(v) = value.as_f64() { Some(am::ScalarValue::Timestamp(v as i64)) } else if let Ok(d) = value.clone().dyn_into::() { Some(am::ScalarValue::Timestamp(d.get_time() as i64)) } else { None } } Some("null") => Some(am::ScalarValue::Null), Some(_) => None, None => { if value.is_null() { Some(am::ScalarValue::Null) } else if let Some(b) = value.as_bool() { Some(am::ScalarValue::Boolean(b)) } else if let Some(s) = value.as_string() { Some(am::ScalarValue::Str(s.into())) } else if let Some(n) = value.as_f64() { if (n.round() - n).abs() < f64::EPSILON { Some(am::ScalarValue::Int(n as i64)) } else { Some(am::ScalarValue::F64(n)) } } else if let Ok(d) = value.clone().dyn_into::() { Some(am::ScalarValue::Timestamp(d.get_time() as i64)) } else if let Ok(o) = &value.clone().dyn_into::() { Some(am::ScalarValue::Bytes(o.to_vec())) } else { None } } } } fn import_value( &self, value: &JsValue, datatype: Option, ) -> Result<(Value<'static>, Vec<(Prop, JsValue)>), JsValue> { match self.import_scalar(value, &datatype) { Some(val) => Ok((val.into(), vec![])), None => { if let Some((o, subvals)) = to_objtype(value, &datatype) { Ok((o.into(), subvals)) } else { web_sys::console::log_2(&"Invalid value".into(), value); Err(to_js_err("invalid value")) } } } } } #[wasm_bindgen(js_name = create)] pub fn init(actor: Option) -> Result { console_error_panic_hook::set_once(); Automerge::new(actor) } #[wasm_bindgen(js_name = load)] pub fn load(data: Uint8Array, actor: Option) -> Result { let data = data.to_vec(); let mut doc = AutoCommit::load(&data).map_err(to_js_err)?; if let Some(s) = actor { let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); doc.set_actor(actor); } Ok(Automerge { doc }) } #[wasm_bindgen(js_name = encodeChange)] pub fn encode_change(change: JsValue) -> Result { let change: am::ExpandedChange = change.into_serde().map_err(to_js_err)?; let change: Change = change.into(); Ok(Uint8Array::from(change.raw_bytes())) } #[wasm_bindgen(js_name = decodeChange)] pub fn decode_change(change: Uint8Array) -> Result { let change = Change::from_bytes(change.to_vec()).map_err(to_js_err)?; let change: am::ExpandedChange = change.decode(); JsValue::from_serde(&change).map_err(to_js_err) } #[wasm_bindgen(js_name = initSyncState)] pub fn init_sync_state() -> SyncState { SyncState(am::sync::State::new()) } // this is needed to be compatible with the automerge-js api #[wasm_bindgen(js_name = importSyncState)] pub fn import_sync_state(state: JsValue) -> Result { Ok(SyncState(JS(state).try_into()?)) } // this is needed to be compatible with the automerge-js api #[wasm_bindgen(js_name = exportSyncState)] pub fn export_sync_state(state: SyncState) -> JsValue { JS::from(state.0).into() } #[wasm_bindgen(js_name = encodeSyncMessage)] pub fn encode_sync_message(message: JsValue) -> Result { let heads = js_get(&message, "heads")?.try_into()?; let need = js_get(&message, "need")?.try_into()?; let changes = js_get(&message, "changes")?.try_into()?; let have = js_get(&message, "have")?.try_into()?; Ok(Uint8Array::from( am::sync::Message { heads, need, have, changes, } .encode() .as_slice(), )) } #[wasm_bindgen(js_name = decodeSyncMessage)] pub fn decode_sync_message(msg: Uint8Array) -> Result { let data = msg.to_vec(); let msg = am::sync::Message::decode(&data).map_err(to_js_err)?; let heads = AR::from(msg.heads.as_slice()); let need = AR::from(msg.need.as_slice()); let changes = AR::from(msg.changes.as_slice()); let have = AR::from(msg.have.as_slice()); let obj = Object::new().into(); js_set(&obj, "heads", heads)?; js_set(&obj, "need", need)?; js_set(&obj, "have", have)?; js_set(&obj, "changes", changes)?; Ok(obj) } #[wasm_bindgen(js_name = encodeSyncState)] pub fn encode_sync_state(state: SyncState) -> Result { let state = state.0; Ok(Uint8Array::from(state.encode().as_slice())) } #[wasm_bindgen(js_name = decodeSyncState)] pub fn decode_sync_state(data: Uint8Array) -> Result { SyncState::decode(data) }