diff --git a/automerge-js/package.json b/automerge-js/package.json index 8742d99a..17018429 100644 --- a/automerge-js/package.json +++ b/automerge-js/package.json @@ -10,7 +10,7 @@ "mocha": "^9.1.1" }, "dependencies": { - "automerge-wasm": "file:../automerge-wasm/dev", + "automerge-wasm": "file:../automerge-wasm", "fast-sha256": "^1.3.0", "pako": "^2.0.4", "uuid": "^8.3" diff --git a/automerge-wasm/.gitignore b/automerge-wasm/.gitignore index a5ef445c..90f5b649 100644 --- a/automerge-wasm/.gitignore +++ b/automerge-wasm/.gitignore @@ -1,5 +1,7 @@ /node_modules /dev +/node +/web /target Cargo.lock yarn.lock diff --git a/automerge-wasm/Cargo.toml b/automerge-wasm/Cargo.toml index 81c5a2a3..2ee2b44e 100644 --- a/automerge-wasm/Cargo.toml +++ b/automerge-wasm/Cargo.toml @@ -3,7 +3,7 @@ name = "automerge-wasm" description = "An js/wasm wrapper for the rust implementation of automerge-backend" # repository = "https://github.com/automerge/automerge-rs" -version = "0.1.0" +version = "0.0.4" authors = ["Alex Good ","Orion Henry ", "Martin Kleppmann"] categories = ["wasm"] readme = "README.md" @@ -40,10 +40,10 @@ version = "^0.2" features = ["serde-serialize", "std"] [package.metadata.wasm-pack.profile.release] -# wasm-opt = false +wasm-opt = true [package.metadata.wasm-pack.profile.profiling] -wasm-opt = false +wasm-opt = true # The `web-sys` crate allows you to interact with the various browser APIs, # like the DOM. diff --git a/automerge-wasm/README.md b/automerge-wasm/README.md index 63548307..80f8f1fa 100644 --- a/automerge-wasm/README.md +++ b/automerge-wasm/README.md @@ -2,695 +2,3 @@ This is a low level automerge library written in rust exporting a javascript API via WASM. This low level api is the underpinning to the `automerge-js` library that reimplements the Automerge API via these low level functions. -### Static Functions - -### Methods - -`doc.clone(actor?: string)` : Make a complete - -`doc.free()` : deallocate WASM memory associated with a document - -```rust - #[wasm_bindgen] - pub fn free(self) {} - - #[wasm_bindgen(js_name = pendingOps)] - pub fn pending_ops(&self) -> JsValue { - (self.0.pending_ops() as u32).into() - } - - pub fn commit(&mut self, message: Option, time: Option) -> Array { - let heads = self.0.commit(message, time.map(|n| n as i64)); - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - heads - } - - pub fn rollback(&mut self) -> f64 { - self.0.rollback() as f64 - } - - pub fn keys(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - let result = if let Some(heads) = get_heads(heads) { - self.0.keys_at(&obj, &heads) - } else { - self.0.keys(&obj) - } - .iter() - .map(|s| JsValue::from_str(s)) - .collect(); - Ok(result) - } - - pub fn text(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - if let Some(heads) = get_heads(heads) { - self.0.text_at(&obj, &heads) - } else { - self.0.text(&obj) - } - .map_err(to_js_err) - } - - pub fn splice( - &mut self, - obj: String, - 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.0 - .splice_text(&obj, start, delete_count, &t) - .map_err(to_js_err)?; - Ok(None) - } else { - if let Ok(array) = text.dyn_into::() { - for i in array.iter() { - if let Ok(array) = i.clone().dyn_into::() { - let value = array.get(1); - let datatype = array.get(2); - let value = self.import_value(value, datatype.as_string())?; - vals.push(value); - } else { - let value = self.import_value(i, None)?; - vals.push(value); - } - } - } - let result = self - .0 - .splice(&obj, start, delete_count, vals) - .map_err(to_js_err)?; - if result.is_empty() { - Ok(None) - } else { - let result: Array = result - .iter() - .map(|r| JsValue::from(r.to_string())) - .collect(); - Ok(result.into()) - } - } - } - - pub fn push( - &mut self, - obj: String, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let value = self.import_value(value, datatype)?; - let index = self.0.length(&obj); - let opid = self.0.insert(&obj, index, value).map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn insert( - &mut self, - obj: String, - index: f64, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let index = index as f64; - let value = self.import_value(value, datatype)?; - let opid = self - .0 - .insert(&obj, index as usize, value) - .map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn set( - &mut self, - obj: String, - prop: JsValue, - value: JsValue, - datatype: Option, - ) -> Result, JsValue> { - let obj = self.import(obj)?; - let prop = self.import_prop(prop)?; - let value = self.import_value(value, datatype)?; - let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?; - Ok(opid.map(|id| id.to_string())) - } - - pub fn make( - &mut self, - obj: String, - prop: JsValue, - value: JsValue, - ) -> Result { - let obj = self.import(obj)?; - let prop = self.import_prop(prop)?; - let value = self.import_value(value, None)?; - if value.is_object() { - let opid = self.0.set(&obj, prop, value).map_err(to_js_err)?; - Ok(opid.unwrap().to_string()) - } else { - Err("invalid object type".into()) - } - } - - pub fn inc(&mut self, obj: String, 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("inc needs a numberic value") - .map_err(to_js_err)?; - self.0.inc(&obj, prop, value as i64).map_err(to_js_err)?; - Ok(()) - } - - pub fn value( - &mut self, - obj: String, - 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.0.value_at(&obj, prop, &h) - } else { - self.0.value(&obj, prop) - } - .map_err(to_js_err)?; - match value { - Some((Value::Object(obj_type), obj_id)) => { - result.push(&obj_type.to_string().into()); - result.push(&obj_id.to_string().into()); - } - Some((Value::Scalar(value), _)) => { - result.push(&datatype(&value).into()); - result.push(&ScalarValue(value).into()); - } - None => {} - } - } - Ok(result) - } - - pub fn values( - &mut self, - obj: String, - 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.0.values_at(&obj, prop, &heads) - } else { - self.0.values(&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) - } - - pub fn length(&mut self, obj: String, heads: Option) -> Result { - let obj = self.import(obj)?; - if let Some(heads) = get_heads(heads) { - Ok(self.0.length_at(&obj, &heads) as f64) - } else { - Ok(self.0.length(&obj) as f64) - } - } - - pub fn del(&mut self, obj: String, prop: JsValue) -> Result<(), JsValue> { - let obj = self.import(obj)?; - let prop = to_prop(prop)?; - self.0.del(&obj, prop).map_err(to_js_err)?; - Ok(()) - } - - pub fn mark( - &mut self, - obj: JsValue, - range: JsValue, - name: JsValue, - value: JsValue, - datatype: JsValue, - ) -> Result<(), JsValue> { - let obj = self.import(obj)?; - let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); - let range = range.as_string().ok_or("range must be a string")?; - let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; - let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; - let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; - let start_sticky = &cap[1] == "("; - let end_sticky = &cap[4] == ")"; - let name = name - .as_string() - .ok_or("invalid mark name") - .map_err(to_js_err)?; - let value = self.import_scalar(&value, datatype.as_string())?; - self.0 - .mark(&obj, start, start_sticky, end, end_sticky, &name, value) - .map_err(to_js_err)?; - Ok(()) - } - - pub fn spans(&mut self, obj: JsValue) -> Result { - let obj = self.import(obj)?; - let text = self.0.text(&obj).map_err(to_js_err)?; - let spans = self.0.spans(&obj).map_err(to_js_err)?; - let mut last_pos = 0; - let result = Array::new(); - for s in spans { - let marks = Array::new(); - for m in s.marks { - let mark = Array::new(); - mark.push(&m.0.into()); - mark.push(&datatype(&m.1).into()); - mark.push(&ScalarValue(m.1).into()); - marks.push(&mark.into()); - } - let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); - if text_span.len() > 0 { - result.push(&text_span.into()); - } - result.push(&marks); - last_pos = s.pos; - //let obj = Object::new().into(); - //js_set(&obj, "pos", s.pos as i32)?; - //js_set(&obj, "marks", marks)?; - //result.push(&obj.into()); - } - let text_span = &text[last_pos..]; - if text_span.len() > 0 { - result.push(&text_span.into()); - } - Ok(result.into()) - } - - pub fn save(&mut self) -> Result { - self.0 - .save() - .map(|v| Uint8Array::from(v.as_slice())) - .map_err(to_js_err) - } - - #[wasm_bindgen(js_name = saveIncremental)] - pub fn save_incremental(&mut self) -> Uint8Array { - let bytes = self.0.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.0.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.0.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.0.get_changes(&deps); - let changes: Array = changes - .iter() - .map(|c| Uint8Array::from(c.raw_bytes())) - .collect(); - Ok(changes) - } - - #[wasm_bindgen(js_name = getChangesAdded)] - pub fn get_changes_added(&mut self, other: &Automerge) -> Result { - let changes = self.0.get_changes_added(&other.0); - 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.0.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(&mut self) -> String { - let actor = self.0.get_actor(); - actor.to_string() - } - - #[wasm_bindgen(js_name = getLastLocalChange)] - pub fn get_last_local_change(&mut self) -> Result, JsValue> { - if let Some(change) = self.0.get_last_local_change() { - Ok(Some(Uint8Array::from(change.raw_bytes()))) - } else { - Ok(None) - } - } - - pub fn dump(&self) { - self.0.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.0.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::SyncMessage::decode(message.as_slice()).map_err(to_js_err)?; - self.0 - .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.0.generate_sync_message(&mut state.0) { - Ok(Uint8Array::from(message.encode().map_err(to_js_err)?.as_slice()).into()) - } else { - Ok(JsValue::null()) - } - } - - #[wasm_bindgen(js_name = toJS)] - pub fn to_js(&self) -> JsValue { - map_to_js(&self.0, ROOT) - } - - fn import(&self, id: String) -> Result { - self.0.import(&id).map_err(to_js_err) - } - - fn import_prop(&mut 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(format!("invalid prop {:?}", prop).into()) - } - } - - fn import_scalar( - &mut self, - value: &JsValue, - datatype: Option, - ) -> Result { - match datatype.as_deref() { - Some("boolean") => value - .as_bool() - .ok_or_else(|| "value must be a bool".into()) - .map(am::ScalarValue::Boolean), - Some("int") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Int(v as i64)), - Some("uint") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Uint(v as u64)), - Some("f64") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(am::ScalarValue::F64), - Some("bytes") => Ok(am::ScalarValue::Bytes( - value.clone().dyn_into::().unwrap().to_vec(), - )), - Some("counter") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::counter(v as i64)), - Some("timestamp") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Timestamp(v as i64)), - /* - Some("bytes") => unimplemented!(), - Some("cursor") => unimplemented!(), - */ - Some("null") => Ok(am::ScalarValue::Null), - Some(_) => Err(format!("unknown datatype {:?}", datatype).into()), - None => { - if value.is_null() { - Ok(am::ScalarValue::Null) - } else if let Some(b) = value.as_bool() { - Ok(am::ScalarValue::Boolean(b)) - } else if let Some(s) = value.as_string() { - // FIXME - we need to detect str vs int vs float vs bool here :/ - Ok(am::ScalarValue::Str(s.into())) - } else if let Some(n) = value.as_f64() { - if (n.round() - n).abs() < f64::EPSILON { - Ok(am::ScalarValue::Int(n as i64)) - } else { - Ok(am::ScalarValue::F64(n)) - } - // } else if let Some(o) = to_objtype(&value) { - // Ok(o.into()) - } else if let Ok(d) = value.clone().dyn_into::() { - Ok(am::ScalarValue::Timestamp(d.get_time() as i64)) - } else if let Ok(o) = &value.clone().dyn_into::() { - Ok(am::ScalarValue::Bytes(o.to_vec())) - } else { - Err("value is invalid".into()) - } - } - } - } - - fn import_value(&mut self, value: JsValue, datatype: Option) -> Result { - match self.import_scalar(&value, datatype) { - Ok(val) => Ok(val.into()), - Err(err) => { - if let Some(o) = to_objtype(&value) { - Ok(o.into()) - } else { - Err(err) - } - } - } - /* - match datatype.as_deref() { - Some("boolean") => value - .as_bool() - .ok_or_else(|| "value must be a bool".into()) - .map(|v| am::ScalarValue::Boolean(v).into()), - Some("int") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Int(v as i64).into()), - Some("uint") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Uint(v as u64).into()), - Some("f64") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|n| am::ScalarValue::F64(n).into()), - Some("bytes") => { - Ok(am::ScalarValue::Bytes(value.dyn_into::().unwrap().to_vec()).into()) - } - Some("counter") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::counter(v as i64).into()), - Some("timestamp") => value - .as_f64() - .ok_or_else(|| "value must be a number".into()) - .map(|v| am::ScalarValue::Timestamp(v as i64).into()), - Some("null") => Ok(am::ScalarValue::Null.into()), - Some(_) => Err(format!("unknown datatype {:?}", datatype).into()), - None => { - if value.is_null() { - Ok(am::ScalarValue::Null.into()) - } else if let Some(b) = value.as_bool() { - Ok(am::ScalarValue::Boolean(b).into()) - } else if let Some(s) = value.as_string() { - // FIXME - we need to detect str vs int vs float vs bool here :/ - Ok(am::ScalarValue::Str(s.into()).into()) - } else if let Some(n) = value.as_f64() { - if (n.round() - n).abs() < f64::EPSILON { - Ok(am::ScalarValue::Int(n as i64).into()) - } else { - Ok(am::ScalarValue::F64(n).into()) - } - } else if let Some(o) = to_objtype(&value) { - Ok(o.into()) - } else if let Ok(d) = value.clone().dyn_into::() { - Ok(am::ScalarValue::Timestamp(d.get_time() as i64).into()) - } else if let Ok(o) = &value.dyn_into::() { - Ok(am::ScalarValue::Bytes(o.to_vec()).into()) - } else { - Err("value is invalid".into()) - } - } - } - */ - } - -} - -#[wasm_bindgen(js_name = create)] -pub fn init(actor: Option) -> Result { - console_error_panic_hook::set_once(); - Automerge::new(actor) -} - -#[wasm_bindgen(js_name = loadDoc)] -pub fn load(data: Uint8Array, actor: Option) -> Result { - let data = data.to_vec(); - let mut automerge = am::Automerge::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()); - automerge.set_actor(actor) - } - Ok(Automerge(automerge)) -} - -#[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::SyncState::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::SyncMessage { - heads, - need, - have, - changes, - } - .encode() - .unwrap() - .as_slice(), - )) -} - -#[wasm_bindgen(js_name = decodeSyncMessage)] -pub fn decode_sync_message(msg: Uint8Array) -> Result { - let data = msg.to_vec(); - let msg = am::SyncMessage::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().map_err(to_js_err)?.as_slice(), - )) -} - -#[wasm_bindgen(js_name = decodeSyncState)] -pub fn decode_sync_state(data: Uint8Array) -> Result { - SyncState::decode(data) -} - -#[wasm_bindgen(js_name = MAP)] -pub struct Map {} - -#[wasm_bindgen(js_name = LIST)] -pub struct List {} - -#[wasm_bindgen(js_name = TEXT)] -pub struct Text {} - -#[wasm_bindgen(js_name = TABLE)] -pub struct Table {} -``` diff --git a/automerge-wasm/attr_bug.js b/automerge-wasm/attr_bug.js new file mode 100644 index 00000000..324fba33 --- /dev/null +++ b/automerge-wasm/attr_bug.js @@ -0,0 +1,15 @@ +let Automerge = require(".") +let util = require('util') + +let heads = ['d138235e8123c407852968a976bb3d05bb30b9f7639854e64cb4adee98a407a6'] +let newHeads = ['d2a0500dad1b4ef1ca0f66015ae24f5cd7bec8316aa8e1115640a665e188147e'] +let text = '10@e1761c3ec92a87d3620d1bc007bdf83a000015ca0b60684edfd007672a0f00113ba1' +let data = '133,111,74,131,126,182,225,217,0,130,22,22,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,45,11,84,68,75,148,168,76,245,27,147,189,91,99,157,102,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,60,72,31,34,255,16,190,226,176,124,232,19,117,181,152,202,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,173,17,57,82,13,196,120,217,253,4,117,222,120,203,127,31,34,0,174,8,20,12,38,118,140,95,76,123,139,6,212,187,22,0,0,195,238,208,1,215,183,150,181,230,202,10,131,10,53,212,98,16,118,64,44,216,205,38,70,50,172,104,141,96,213,70,225,153,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,7,90,18,166,242,242,169,181,172,173,95,218,197,230,53,171,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,20,123,52,22,113,155,106,167,61,96,211,220,13,176,202,18,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,21,202,11,96,104,78,223,208,7,103,42,15,0,17,59,161,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,49,157,99,144,176,89,107,142,238,50,16,33,198,172,12,98,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,49,160,189,244,223,205,155,34,245,110,74,38,170,63,47,165,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,101,43,36,88,127,139,248,176,98,81,75,151,178,155,65,235,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,104,72,125,26,22,39,88,236,174,2,180,0,186,44,23,100,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,106,192,146,37,220,38,124,176,133,96,99,183,52,146,51,32,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,137,185,129,79,171,192,93,254,162,191,198,11,166,169,184,231,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,183,221,99,120,31,214,103,85,152,145,225,205,226,10,71,148,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,204,247,249,8,135,23,98,57,29,144,111,93,62,1,176,68,34,225,118,28,62,201,42,135,211,98,13,27,192,7,189,248,58,0,0,243,90,241,176,57,235,58,247,98,38,71,96,245,193,178,119,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,23,61,123,236,184,3,106,194,171,46,241,84,223,211,110,241,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,32,181,113,40,11,161,118,67,217,36,93,201,189,221,55,174,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,97,188,15,173,96,163,123,87,228,32,227,245,56,237,53,228,34,229,150,245,136,76,151,59,113,93,112,149,234,7,68,20,213,0,0,97,221,248,228,210,133,45,170,105,131,177,2,9,124,254,61,16,255,46,217,125,15,181,79,74,181,101,95,13,121,190,236,160,1,210,160,80,13,173,27,78,241,202,15,102,1,90,226,79,92,215,190,200,49,106,168,225,17,86,64,166,101,225,136,20,126,8,1,48,3,78,19,38,35,3,53,77,64,17,67,20,86,3,14,1,11,2,17,17,165,1,27,242,1,21,202,1,33,238,1,43,187,2,52,4,66,51,86,78,95,177,2,128,1,45,129,1,15,131,1,23,127,7,21,4,24,8,12,15,3,14,15,6,193,0,12,8,13,3,10,127,9,6,11,127,16,3,2,20,3,10,5,33,3,126,5,17,17,1,11,17,24,21,12,0,125,18,20,19,126,1,0,20,1,127,108,23,1,127,105,11,1,127,117,2,1,127,126,14,1,127,114,192,0,1,127,64,7,1,127,121,2,1,126,126,0,5,1,126,123,0,2,1,127,126,19,1,127,109,9,1,127,11,32,1,125,86,118,0,16,1,127,113,10,1,127,117,23,1,127,105,11,1,127,117,2,0,127,14,2,3,16,1,127,3,2,1,127,2,9,1,2,2,14,1,121,2,1,2,1,2,1,2,171,1,1,127,2,28,1,126,0,4,38,1,167,2,0,255,1,0,127,70,123,34,97,117,116,104,111,114,73,100,34,58,34,101,49,55,54,49,99,51,101,99,57,50,97,56,55,100,51,54,50,48,100,49,98,99,48,48,55,98,100,102,56,51,97,34,44,34,109,101,115,115,97,103,101,34,58,34,74,97,102,102,97,32,67,97,107,101,34,125,39,0,127,0,192,1,1,127,2,32,1,127,2,18,1,127,2,49,1,127,0,191,1,1,126,119,10,33,1,126,95,34,17,1,126,112,17,49,1,167,2,7,0,17,161,2,7,127,4,3,8,4,15,0,17,3,9,157,2,10,119,12,19,42,53,55,71,74,77,80,0,21,5,7,124,6,7,15,7,3,4,127,3,4,4,127,6,7,4,124,6,12,3,4,2,15,127,3,2,15,127,6,15,3,127,6,14,3,11,21,2,3,127,6,11,4,126,6,12,15,1,5,4,126,12,3,13,4,127,14,3,4,2,14,124,6,12,3,4,3,8,127,10,9,17,126,8,12,10,21,126,8,6,5,8,127,3,3,8,127,12,8,13,3,8,127,6,6,8,2,14,124,6,12,3,8,4,15,127,6,2,8,123,15,12,3,15,6,2,8,127,12,2,2,123,8,6,12,3,8,4,15,127,6,6,8,125,12,10,3,5,8,127,6,2,8,11,0,16,8,127,6,2,12,127,9,6,11,126,16,3,2,4,0,10,69,142,189,75,66,97,28,133,207,121,223,50,149,140,140,43,213,32,45,125,64,208,93,226,6,81,75,83,67,4,145,91,91,67,91,229,216,7,250,187,16,33,53,93,250,35,154,171,185,177,32,130,162,156,36,18,84,156,28,85,80,55,125,95,21,228,89,14,156,7,206,129,35,152,28,195,113,30,79,254,230,110,8,250,133,63,2,226,23,207,62,97,121,67,162,128,188,184,184,103,91,48,27,112,138,82,149,46,98,244,189,40,211,59,108,202,235,80,181,124,192,185,179,74,32,84,56,225,63,62,81,148,65,165,161,76,114,15,21,214,48,190,250,142,178,85,36,199,10,194,204,62,72,17,143,140,48,211,202,122,123,74,243,58,96,221,28,249,195,66,136,87,184,49,11,235,251,70,191,32,22,161,189,173,154,36,53,206,166,83,42,254,5,55,231,39,15,88,198,10,59,178,180,189,81,147,121,83,57,41,104,5,150,48,23,239,244,247,151,143,194,13,70,121,122,43,151,163,183,150,196,55,24,155,96,102,166,32,233,115,68,122,127,8,97,114,99,104,105,118,101,100,2,6,97,117,116,104,111,114,126,8,99,111,109,109,101,110,116,115,12,99,111,110,116,114,105,98,117,116,111,114,115,3,7,109,101,115,115,97,103,101,2,9,112,97,114,101,110,116,95,105,100,125,6,112,105,110,110,101,100,6,115,104,97,114,101,100,4,116,101,120,116,3,4,116,105,109,101,124,5,116,105,116,108,101,32,48,48,97,101,48,56,49,52,48,99,50,54,55,54,56,99,53,102,52,99,55,98,56,98,48,54,100,52,98,98,49,54,32,101,49,55,54,49,99,51,101,99,57,50,97,56,55,100,51,54,50,48,100,49,98,99,48,48,55,98,100,102,56,51,97,32,101,53,57,54,102,53,56,56,52,99,57,55,51,98,55,49,53,100,55,48,57,53,101,97,48,55,52,52,49,52,100,53,0,157,2,9,4,116,121,112,101,2,7,127,21,3,7,124,4,21,4,21,4,7,119,4,21,7,1,7,17,7,5,3,2,12,121,6,12,15,12,4,12,4,2,3,111,12,4,12,6,12,4,12,4,12,4,12,6,12,3,5,15,5,2,3,126,12,6,30,3,12,21,2,3,115,12,4,12,4,12,4,12,4,12,4,12,6,12,16,1,123,4,10,12,4,12,2,3,102,4,12,4,12,4,12,4,12,4,12,4,12,14,12,4,12,14,12,6,12,3,5,8,5,3,10,10,17,127,12,11,21,126,6,12,4,8,2,3,125,12,8,12,7,13,102,5,13,8,12,6,12,8,5,12,8,12,14,12,6,12,3,5,15,5,3,12,6,12,8,15,12,2,3,124,6,12,8,12,3,2,117,6,12,3,5,15,5,3,12,6,12,15,2,8,124,12,8,12,10,2,3,121,8,12,8,12,6,12,8,12,0,112,12,8,12,8,12,8,12,8,12,8,12,8,12,8,12,6,2,12,127,9,6,11,125,16,3,5,2,4,2,7,127,4,3,8,4,15,117,143,207,43,131,113,28,199,63,239,103,101,241,236,49,19,113,115,81,147,178,56,108,10,7,92,108,56,40,162,20,218,193,109,139,210,52,179,231,251,41,23,148,103,59,40,7,57,176,54,155,205,197,143,63,65,106,53,57,49,108,53,187,56,104,23,66,118,179,135,92,148,195,235,211,235,240,254,244,249,188,157,85,193,93,169,164,194,95,187,37,125,168,154,244,164,178,113,85,214,164,103,181,140,144,120,129,38,46,193,147,253,203,242,76,235,17,223,81,138,79,128,40,167,248,150,2,198,36,103,233,23,79,99,150,228,55,161,33,104,218,97,79,24,102,176,91,1,219,101,44,14,96,157,227,252,64,127,241,54,108,84,98,179,97,177,13,231,33,231,40,77,200,139,28,233,170,147,224,123,210,79,234,209,137,14,125,142,219,166,218,47,200,116,35,142,177,226,46,82,53,68,73,196,80,3,53,41,218,98,108,64,32,12,184,244,181,150,42,204,93,211,41,175,85,124,218,26,231,12,66,223,31,12,119,143,218,123,149,178,216,132,111,112,172,43,202,150,12,217,16,225,206,3,126,36,171,132,249,61,238,115,40,239,149,18,75,174,17,199,25,123,165,2,69,184,64,205,22,124,138,31,29,234,73,240,43,100,120,243,98,159,139,244,31,231,124,69,80,140,240,213,155,211,98,193,47,25,155,100,169,206,96,248,2,20,157,2,9,3,1,2,0,7,1,127,4,7,1,127,0,28,1,127,0,235,0,1,127,0,43,1,127,7,6,1,127,9,9,1,127,0,5,1,127,0,15,1,127,0,5,1,2,0,55,1,127,0,10,1,127,1,2,134,4,2,0,125,230,1,166,1,182,1,2,214,2,2,1,126,0,105,2,100,127,6,3,2,127,0,28,22,127,0,30,22,127,38,204,0,22,127,0,43,22,125,102,2,6,6,22,127,2,9,22,127,0,5,22,127,0,15,22,127,0,5,22,2,0,54,22,125,38,0,22,9,150,1,173,80,75,78,195,48,16,189,74,110,208,184,78,234,100,87,17,85,21,16,169,5,129,248,108,208,56,246,164,85,211,16,98,47,210,172,145,216,245,2,108,122,150,46,144,224,74,116,193,36,161,233,5,106,217,158,55,243,230,243,108,205,196,136,37,92,39,225,16,2,161,248,104,232,42,38,19,215,21,82,97,192,65,251,225,8,253,32,240,146,80,112,41,152,175,132,27,250,26,92,225,121,204,83,254,253,252,97,18,199,47,183,179,217,221,120,60,190,2,68,112,34,88,233,233,50,79,117,41,75,13,10,163,236,102,184,201,46,39,69,52,227,79,38,136,179,231,183,245,133,143,108,90,101,139,98,190,102,122,163,217,99,140,215,209,251,215,246,251,119,240,209,222,135,159,207,237,97,32,107,107,43,203,44,88,131,96,149,229,22,27,76,177,90,202,26,201,46,150,198,161,13,14,161,60,117,122,127,191,107,2,70,33,28,15,145,170,45,111,110,110,13,41,51,10,254,121,84,176,34,138,16,145,202,58,132,129,186,55,147,186,89,85,223,135,90,244,80,85,173,131,77,54,118,203,216,242,53,79,201,240,118,208,177,158,140,75,237,121,151,216,199,154,199,164,8,157,164,147,86,210,211,82,39,193,21,125,4,45,206,121,85,203,253,206,41,160,132,180,132,98,113,46,240,7,126,0,1,3,0,2,1,126,0,1,4,0,2,1,51,0,127,1,10,0,127,1,219,0,0,127,1,15,0,3,1,36,0,127,1,21,0,2,1,54,0,127,1,11,0,126,21,4,2,21,123,4,21,18,19,20,6,15,127,4,118,156,2,243,125,140,2,3,242,125,141,2,37,2,127,145,126,2,127,3,125,127,92' + +let doc = Automerge.loadDoc(new Uint8Array(data.toString().split(",").map((n) => parseInt(n)))) + +console.log(doc.text(text,heads)) +console.log(doc.text(text,newHeads)) +console.log(doc.text(text)) +console.log(util.inspect(doc.attribute(text,heads,[newHeads]), false, null, false)) + diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index ab43d968..04373f11 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -62,6 +62,23 @@ export type DecodedChange = { ops: Op[] } +export type ChangeSetAddition = { + actor: string, + start: number, + end: number, +} + +export type ChangeSetDeletion = { + actor: string, + pos: number, + val: string +} + +export type ChangeSet = { + add: ChangeSetAddition[], + del: ChangeSetDeletion[] +} + export type Op = { action: string, obj: ObjID, @@ -102,9 +119,18 @@ export class Automerge { length(obj: ObjID, heads?: Heads): number; materialize(obj?: ObjID): any; + // experimental spans api - unstable! + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + unmark(obj: ObjID, mark: ObjID): void; + spans(obj: ObjID): any; + raw_spans(obj: ObjID): any; + blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + attribute2(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; + // transactions commit(message?: string, time?: number): Heads; - merge(other: Automerge): Heads; + merge(other: Automerge): ObjID[]; getActorId(): Actor; pendingOps(): number; rollback(): number; @@ -112,14 +138,14 @@ export class Automerge { // save and load to local store save(): Uint8Array; saveIncremental(): Uint8Array; - loadIncremental(data: Uint8Array): number; + loadIncremental(data: Uint8Array): ObjID[]; // sync over network - receiveSyncMessage(state: SyncState, message: SyncMessage): void; + receiveSyncMessage(state: SyncState, message: SyncMessage): ObjID[]; generateSyncMessage(state: SyncState): SyncMessage | null; // low level change functions - applyChanges(changes: Change[]): void; + applyChanges(changes: Change[]): ObjID[]; getChanges(have_deps: Heads): Change[]; getChangeByHash(hash: Hash): Change | null; getChangesAdded(other: Automerge): Change[]; diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 92eb79f8..336f78f6 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -4,24 +4,27 @@ "Alex Good ", "Martin Kleppmann" ], - "name": "automerge-wasm", + "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.1", + "version": "0.0.23", "license": "MIT", "files": [ "README.md", - "LICENSE", "package.json", - "automerge_wasm_bg.wasm", - "automerge_wasm.js" + "index.d.ts", + "node/index.js", + "node/index_bg.wasm", + "web/index.js", + "web/index_bg.wasm" ], - "module": "./pkg/index.js", - "main": "./dev/index.js", + "types": "index.d.ts", + "module": "./web/index.js", + "main": "./node/index.js", "scripts": { - "build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev && cp index.d.ts dev", - "release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && cp index.d.ts dev", - "pkg": "rimraf ./pkg && wasm-pack build --target web --release --out-name index -d pkg && cp index.d.ts pkg && cd pkg && yarn pack && mv automerge-wasm*tgz ..", - "prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev", + "build": "rimraf ./node && wasm-pack build --target nodejs --dev --out-name index -d node && cp index.d.ts node", + "release-w": "rimraf ./web && wasm-pack build --target web --release --out-name index -d web && cp index.d.ts web", + "release-n": "rimraf ./node && wasm-pack build --target nodejs --release --out-name index -d node && cp index.d.ts node", + "release": "yarn release-w && yarn release-n", "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, "dependencies": {}, diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index 69dd38f7..4fec2359 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -329,6 +329,15 @@ pub(crate) fn get_heads(heads: Option) -> Option> { heads.ok() } +pub(crate) fn get_js_heads(heads: JsValue) -> Result, JsValue> { + let heads = heads.dyn_into::()?; + heads + .iter() + .map(|j| j.into_serde()) + .collect::, _>>() + .map_err(to_js_err) +} + pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue { let keys = doc.keys(obj); let map = Object::new(); diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 14c631fb..8e8dce53 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -4,6 +4,7 @@ use am::transaction::Transactable; use automerge as am; use automerge::{Change, ObjId, Prop, Value, ROOT}; use js_sys::{Array, Object, Uint8Array}; +use regex::Regex; use std::convert::TryInto; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -13,7 +14,8 @@ mod sync; mod value; use interop::{ - get_heads, js_get, js_set, list_to_js, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, + get_heads, get_js_heads, js_get, js_set, list_to_js, map_to_js, to_js_err, to_objtype, to_prop, + AR, JS, }; use sync::SyncState; use value::{datatype, ScalarValue}; @@ -89,12 +91,9 @@ impl Automerge { } pub fn merge(&mut self, other: &mut Automerge) -> Result { - let heads = self.0.merge(&mut other.0)?; - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - Ok(heads) + let objs = self.0.merge(&mut other.0)?; + let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); + Ok(objs) } pub fn rollback(&mut self) -> f64 { @@ -217,6 +216,18 @@ impl Automerge { Ok(()) } + pub fn make( + &mut self, + obj: JsValue, + prop: JsValue, + value: JsValue, + _datatype: JsValue, + ) -> Result { + // remove this + am::log!("doc.make() is depricated - please use doc.set_object() or doc.insert_object()"); + self.set_object(obj, prop, value) + } + pub fn set_object( &mut self, obj: JsValue, @@ -354,6 +365,209 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: JsValue, + range: JsValue, + name: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result<(), JsValue> { + let obj = self.import(obj)?; + let re = Regex::new(r"([\[\(])(\d+)\.\.(\d+)([\)\]])").unwrap(); + let range = range.as_string().ok_or("range must be a string")?; + let cap = re.captures_iter(&range).next().ok_or("range must be in the form of (start..end] or [start..end) etc... () for sticky, [] for normal")?; + let start: usize = cap[2].parse().map_err(|_| to_js_err("invalid start"))?; + let end: usize = cap[3].parse().map_err(|_| to_js_err("invalid end"))?; + let start_sticky = &cap[1] == "("; + let end_sticky = &cap[4] == ")"; + let name = name + .as_string() + .ok_or("invalid mark name") + .map_err(to_js_err)?; + let value = self + .import_scalar(&value, &datatype.as_string()) + .ok_or_else(|| to_js_err("invalid value"))?; + self.0 + .mark(&obj, start, start_sticky, end, end_sticky, &name, value) + .map_err(to_js_err)?; + Ok(()) + } + + pub fn unmark(&mut self, obj: JsValue, mark: JsValue) -> Result<(), JsValue> { + let obj = self.import(obj)?; + let mark = self.import(mark)?; + self.0.unmark(&obj, &mark).map_err(to_js_err)?; + Ok(()) + } + + pub fn spans(&mut self, obj: JsValue) -> Result { + let obj = self.import(obj)?; + let text = self.0.list(&obj).map_err(to_js_err)?; + let spans = self.0.spans(&obj).map_err(to_js_err)?; + let mut last_pos = 0; + let result = Array::new(); + for s in spans { + let marks = Array::new(); + for m in s.marks { + let mark = Array::new(); + mark.push(&m.0.into()); + mark.push(&datatype(&m.1).into()); + mark.push(&ScalarValue(m.1).into()); + marks.push(&mark.into()); + } + let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); + if !text_span.is_empty() { + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); + } + result.push(&marks); + last_pos = s.pos; + //let obj = Object::new().into(); + //js_set(&obj, "pos", s.pos as i32)?; + //js_set(&obj, "marks", marks)?; + //result.push(&obj.into()); + } + let text_span = &text[last_pos..]; + if !text_span.is_empty() { + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); + } + Ok(result.into()) + } + + pub fn raw_spans(&mut self, obj: JsValue) -> Result { + let obj = self.import(obj)?; + let spans = self.0.raw_spans(&obj).map_err(to_js_err)?; + let result = Array::new(); + for s in spans { + result.push(&JsValue::from_serde(&s).map_err(to_js_err)?); + } + Ok(result) + } + + pub fn blame( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + am::log!("doc.blame() is depricated - please use doc.attribute()"); + self.attribute(obj, baseline, change_sets) + } + + pub fn attribute( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let obj = self.import(obj)?; + let baseline = get_js_heads(baseline)?; + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .map(get_js_heads) + .collect::, _>>()?; + let result = self.0.attribute(&obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|range| { + let r = Object::new(); + js_set(&r, "start", range.start as f64)?; + js_set(&r, "end", range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "pos", d.0 as f64)?; + js_set(&r, "val", &d.1)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } + + pub fn attribute2( + &mut self, + obj: JsValue, + baseline: JsValue, + change_sets: JsValue, + ) -> Result { + let obj = self.import(obj)?; + let baseline = get_js_heads(baseline)?; + let change_sets = change_sets.dyn_into::()?; + let change_sets = change_sets + .iter() + .map(get_js_heads) + .collect::, _>>()?; + let result = self.0.attribute2(&obj, &baseline, &change_sets)?; + let result = result + .into_iter() + .map(|cs| { + let add = cs + .add + .iter() + .map::, _>(|a| { + let r = Object::new(); + js_set(&r, "actor", &self.0.actor_to_str(a.actor))?; + js_set(&r, "start", a.range.start as f64)?; + js_set(&r, "end", a.range.end as f64)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let del = cs + .del + .iter() + .map::, _>(|d| { + let r = Object::new(); + js_set(&r, "actor", &self.0.actor_to_str(d.actor))?; + js_set(&r, "pos", d.pos as f64)?; + js_set(&r, "val", &d.span)?; + Ok(JsValue::from(&r)) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + let obj = Object::new(); + js_set(&obj, "add", add)?; + js_set(&obj, "del", del)?; + Ok(obj.into()) + }) + .collect::, JsValue>>()? + .iter() + .collect::(); + Ok(result) + } + pub fn save(&mut self) -> Uint8Array { Uint8Array::from(self.0.save().as_slice()) } @@ -365,17 +579,19 @@ impl Automerge { } #[wasm_bindgen(js_name = loadIncremental)] - pub fn load_incremental(&mut self, data: Uint8Array) -> Result { + pub fn load_incremental(&mut self, data: Uint8Array) -> Result { let data = data.to_vec(); - let len = self.0.load_incremental(&data).map_err(to_js_err)?; - Ok(len as f64) + let objs = self.0.load_incremental(&data).map_err(to_js_err)?; + let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); + Ok(objs) } #[wasm_bindgen(js_name = applyChanges)] - pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> { + pub fn apply_changes(&mut self, changes: JsValue) -> Result { let changes: Vec<_> = JS(changes).try_into()?; - self.0.apply_changes(changes).map_err(to_js_err)?; - Ok(()) + let objs = self.0.apply_changes(changes).map_err(to_js_err)?; + let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); + Ok(objs) } #[wasm_bindgen(js_name = getChanges)] @@ -455,13 +671,15 @@ impl Automerge { &mut self, state: &mut SyncState, message: Uint8Array, - ) -> Result<(), JsValue> { + ) -> Result { let message = message.to_vec(); let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?; - self.0 + let objs = self + .0 .receive_sync_message(&mut state.0, message) .map_err(to_js_err)?; - Ok(()) + let objs: Array = objs.iter().map(|o| JsValue::from(o.to_string())).collect(); + Ok(objs) } #[wasm_bindgen(js_name = generateSyncMessage)] diff --git a/automerge-wasm/test/attribute.ts b/automerge-wasm/test/attribute.ts new file mode 100644 index 00000000..a83dc5d4 --- /dev/null +++ b/automerge-wasm/test/attribute.ts @@ -0,0 +1,189 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { BloomFilter } from './helpers/sync' +import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage, Hash } from '..' + +describe('Automerge', () => { + describe('attribute', () => { + it('should be able to attribute text segments on change sets', () => { + let doc1 = create() + let text = doc1.set_object("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork(); + doc2.splice(text, 5, 7, " big"); + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork(); + doc3.splice(text, 0, 0, "Well, "); + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, + { add: [ { start: 0, end: 6 } ], del: [] } + ]) + }) + + it('should be able to hand complex attribute change sets', () => { + let doc1 = create("aaaa") + let text = doc1.set_object("_root", "notes","AAAAAA") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 0, 2, "BB"); + doc2.commit() + doc2.splice(text, 2, 2, "BB"); + doc2.commit() + doc2.splice(text, 6, 0, "BB"); + doc2.commit() + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "BBBBAABB") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 1, 1, "C"); + doc3.commit() + doc3.splice(text, 3, 1, "C"); + doc3.commit() + doc3.splice(text, 5, 1, "C"); + doc3.commit() + let h3 = doc3.getHeads(); + // with tombstones its + // AC.AC.AC. + assert.deepEqual(doc3.text(text), "ACACAC") + + doc1.merge(doc2) + + assert.deepEqual(doc1.attribute(text, h1, [h2]), [ + { add: [ {start:0, end: 4}, { start: 6, end: 8 } ], del: [ { pos: 4, val: 'AAAA' } ] }, + ]) + + doc1.merge(doc3) + + assert.deepEqual(doc1.text(text), "BBBBCCACBB") + + // with tombstones its + // BBBB.C..C.AC.BB + assert.deepEqual(doc1.attribute(text, h1, [h2,h3]), [ + { add: [ {start:0, end: 4}, { start: 8, end: 10 } ], del: [ { pos: 4, val: 'A' }, { pos: 5, val: 'AA' }, { pos: 6, val: 'A' } ] }, + { add: [ {start:4, end: 6}, { start: 7, end: 8 } ], del: [ { pos: 5, val: 'A' }, { pos: 6, val: 'A' }, { pos: 8, val: 'A' } ] } + ]) + }) + + it('should not include attribution of text that is inserted and deleted only within change sets', () => { + let doc1 = create() + let text = doc1.set_object("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork(); + doc2.splice(text, 5, 7, " big"); + doc2.splice(text, 9, 0, " bad"); + doc2.splice(text, 9, 4) + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork(); + doc3.splice(text, 0, 0, "Well, HI THERE"); + doc3.splice(text, 6, 8, "") + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, + { add: [ { start: 0, end: 6 } ], del: [] } + ]) + }) + + }) + describe('attribute2', () => { + it('should be able to attribute text segments on change sets', () => { + let doc1 = create("aaaa") + let text = doc1.set_object("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 5, 7, " big"); + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 0, 0, "Well, "); + let doc4 = doc3.fork("dddd") + doc4.splice(text, 0, 0, "Gee, "); + let h3 = doc4.getHeads(); + assert.deepEqual(doc4.text(text), "Gee, Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc4) + assert.deepEqual(doc1.text(text), "Gee, Well, hello big world") + let attribute = doc1.attribute2(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { actor: "bbbb", start: 16, end: 20 } ], del: [ { actor: "bbbb", pos: 20, val: ' little' } ] }, + { add: [ { actor: "dddd", start:0, end: 5 }, { actor: "cccc", start: 5, end: 11 } ], del: [] } + ]) + }) + + it('should not include attribution of text that is inserted and deleted only within change sets', () => { + let doc1 = create("aaaa") + let text = doc1.set_object("_root", "notes","hello little world") + let h1 = doc1.getHeads(); + + let doc2 = doc1.fork("bbbb"); + doc2.splice(text, 5, 7, " big"); + doc2.splice(text, 9, 0, " bad"); + doc2.splice(text, 9, 4) + doc2.text(text) + let h2 = doc2.getHeads(); + assert.deepEqual(doc2.text(text), "hello big world") + + let doc3 = doc1.fork("cccc"); + doc3.splice(text, 0, 0, "Well, HI THERE"); + doc3.splice(text, 6, 8, "") + let h3 = doc3.getHeads(); + assert.deepEqual(doc3.text(text), "Well, hello little world") + + doc1.merge(doc2) + doc1.merge(doc3) + assert.deepEqual(doc1.text(text), "Well, hello big world") + let attribute = doc1.attribute2(text, h1, [h2, h3]) + + assert.deepEqual(attribute, [ + { add: [ { start: 11, end: 15, actor: "bbbb" } ], del: [ { pos: 15, val: ' little', actor: "bbbb" } ] }, + { add: [ { start: 0, end: 6, actor: "cccc" } ], del: [] } + ]) + + let h4 = doc1.getHeads() + + doc3.splice(text, 24, 0, "!!!") + doc1.merge(doc3) + + let h5 = doc1.getHeads() + + assert.deepEqual(doc1.text(text), "Well, hello big world!!!") + attribute = doc1.attribute2(text, h4, [h5]) + + assert.deepEqual(attribute, [ + { add: [ { start: 21, end: 24, actor: "cccc" } ], del: [] }, + { add: [], del: [] } + ]) + }) + }) +}) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts new file mode 100644 index 00000000..73146979 --- /dev/null +++ b/automerge-wasm/test/marks.ts @@ -0,0 +1,203 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, loadDoc, Automerge, encodeChange, decodeChange } from '..' + +describe('Automerge', () => { + describe('marks', () => { + it('should handle marks [..]', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaaA', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'Accc' ]); + }) + + it('should handle marks [..] at the beginning of a string', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.insert(list, 0, "A") + doc2.insert(list, 4, "B") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'A', [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'Bbbbccc' ]); + }) + + it('should handle marks [..] with splice', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 0, 2, "AAA") + doc2.splice(list, 4, 0, "BBB") + doc.merge(doc2) + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'a', [], 'BBBbbbccc' ]); + }) + + it('should handle marks across multiple forks', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[0..3]", "bold", true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ [ [ 'bold', 'boolean', true ] ], 'aaa', [], 'bbbccc' ]); + + let doc2 = doc.fork() + doc2.splice(list, 1, 1, "Z") // replace 'aaa' with 'aZa' inside mark. + + let doc3 = doc.fork() + doc3.insert(list, 0, "AAA") // should not be included in mark. + + doc.merge(doc2) + doc.merge(doc3) + + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'AAA', [ [ 'bold', 'boolean', true ] ], 'aZa', [], 'bbbccc' ]); + }) + + + it('should handle marks with deleted ends [..]', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "[3..6]", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.del(list,5); + doc.del(list,5); + doc.del(list,2); + doc.del(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaA', [ [ 'bold', 'boolean', true ] ], 'b', [], 'Acc' ]) + }) + + it('should handle sticky marks (..)', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.insert(list, 6, "A") + doc.insert(list, 3, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); + }) + + it('should handle sticky marks with deleted ends (..)', () => { + let doc = create() + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "aaabbbccc") + doc.mark(list, "(3..6)", "bold" , true) + let spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'bbb', [], 'ccc' ]); + doc.del(list,5); + doc.del(list,5); + doc.del(list,2); + doc.del(list,2); + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'b', [], 'cc' ]) + doc.insert(list, 3, "A") + doc.insert(list, 2, "A") + spans = doc.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + // make sure save/load can handle marks + + let doc2 = loadDoc(doc.save()) + spans = doc2.spans(list); + assert.deepStrictEqual(spans, [ 'aa', [ [ 'bold', 'boolean', true ] ], 'AbA', [], 'cc' ]) + + assert.deepStrictEqual(doc.getHeads(), doc2.getHeads()) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + + it('should handle overlapping marks', () => { + let doc : Automerge = create("aabbcc") + let list = doc.set_object("_root", "list", "") + doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") + doc.mark(list, "[0..37]", "bold" , true) + doc.mark(list, "[4..19]", "itallic" , true) + doc.mark(list, "[10..13]", "comment" , "foxes are my favorite animal!") + doc.commit("marks"); + let spans = doc.spans(list); + assert.deepStrictEqual(spans, + [ + [ [ 'bold', 'boolean', true ] ], + 'the ', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + 'quick ', + [ + [ 'bold', 'boolean', true ], + [ 'comment', 'str', 'foxes are my favorite animal!' ], + [ 'itallic', 'boolean', true ] + ], + 'fox', + [ [ 'bold', 'boolean', true ], [ 'itallic', 'boolean', true ] ], + ' jumps', + [ [ 'bold', 'boolean', true ] ], + ' over the lazy dog', + [], + ] + ) + let text = doc.text(list); + assert.deepStrictEqual(text, "the quick fox jumps over the lazy dog"); + let raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "41@aabbcc", start: 4, end: 19, type: 'itallic', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + doc.unmark(list, "41@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "39@aabbcc", start: 0, end: 37, type: 'bold', value: true }, + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + // mark sure encode decode can handle marks + + doc.unmark(list, "39@aabbcc") + raw_spans = doc.raw_spans(list); + assert.deepStrictEqual(raw_spans, + [ + { id: "43@aabbcc", start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + let all = doc.getChanges([]) + let decoded = all.map((c) => decodeChange(c)) + let encoded = decoded.map((c) => encodeChange(c)) + let doc2 = create(); + doc2.applyChanges(encoded) + + doc.dump() + doc2.dump() + assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + }) +}) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 4c89e81d..97c667b6 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,9 +3,8 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index' -import { DecodedSyncMessage } from '../index'; -import { Hash } from '../dev/index'; +import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage, Hash } from '..' function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) { const MAX_ITER = 10 @@ -229,7 +228,6 @@ describe('Automerge', () => { let root = "_root"; let text = doc.set_object(root, "text", ""); - if (!text) throw new Error('should not be undefined') doc.splice(text, 0, 0, "hello ") doc.splice(text, 6, 0, ["w","o","r","l","d"]) doc.splice(text, 11, 0, ["!","?"]) @@ -276,8 +274,9 @@ describe('Automerge', () => { let docA = loadDoc(saveA); let docB = loadDoc(saveB); let docC = loadDoc(saveMidway) - docC.loadIncremental(save3) + let touched = docC.loadIncremental(save3) + assert.deepEqual(touched, ["_root"]); assert.deepEqual(docA.keys("_root"), docB.keys("_root")); assert.deepEqual(docA.save(), docB.save()); assert.deepEqual(docA.save(), docC.save()); @@ -344,9 +343,11 @@ describe('Automerge', () => { doc1.set(seq, 0, 20) doc2.set(seq, 0, 0, "counter") doc3.set(seq, 0, 10, "counter") - doc1.applyChanges(doc2.getChanges(doc1.getHeads())) - doc1.applyChanges(doc3.getChanges(doc1.getHeads())) + let touched1 = doc1.applyChanges(doc2.getChanges(doc1.getHeads())) + let touched2 = doc1.applyChanges(doc3.getChanges(doc1.getHeads())) let result = doc1.values(seq, 0) + assert.deepEqual(touched1,["1@aaaa"]) + assert.deepEqual(touched2,["1@aaaa"]) assert.deepEqual(result,[ ['int',20,'3@aaaa'], ['counter',0,'3@bbbb'], @@ -388,6 +389,8 @@ describe('Automerge', () => { assert.deepEqual(change2, null) if (change1 === null) { throw new RangeError("change1 should not be null") } assert.deepEqual(decodeChange(change1).hash, head1[0]) + assert.deepEqual(head1.some((hash) => doc1.getChangeByHash(hash) === null), false) + assert.deepEqual(head2.some((hash) => doc1.getChangeByHash(hash) === null), true) }) it('recursive sets are possible', () => { @@ -475,6 +478,19 @@ describe('Automerge', () => { assert.deepEqual(C.text(At), 'hell! world') }) + it('should return opIds that were changed', () => { + let A = create("aabbcc") + let At = A.set_object('_root', 'list', []) + A.insert('/list', 0, 'a') + A.insert('/list', 1, 'b') + + let B = A.fork() + + A.insert('/list', 2, 'c') + + let opIds = A.merge(B) + assert.equal(opIds.length, 0) + }) }) describe('sync', () => { it('should send a sync message implying no local data', () => { @@ -1087,16 +1103,20 @@ describe('Automerge', () => { m2 = n2.generateSyncMessage(s2) if (m1 === null) { throw new RangeError("message should not be null") } if (m2 === null) { throw new RangeError("message should not be null") } - n1.receiveSyncMessage(s1, m2) - n2.receiveSyncMessage(s2, m1) + let touched1 = n1.receiveSyncMessage(s1, m2) + let touched2 = n2.receiveSyncMessage(s2, m1) + assert.deepEqual(touched1, []); + assert.deepEqual(touched2, []); // Then n1 and n2 send each other their changes, except for the false positive m1 = n1.generateSyncMessage(s1) m2 = n2.generateSyncMessage(s2) if (m1 === null) { throw new RangeError("message should not be null") } if (m2 === null) { throw new RangeError("message should not be null") } - n1.receiveSyncMessage(s1, m2) - n2.receiveSyncMessage(s2, m1) + let touched3 = n1.receiveSyncMessage(s1, m2) + let touched4 = n2.receiveSyncMessage(s2, m1) + assert.deepEqual(touched3, []); + assert.deepEqual(touched4, ["_root"]); assert.strictEqual(decodeSyncMessage(m1).changes.length, 2) // n1c1 and n1c2 assert.strictEqual(decodeSyncMessage(m2).changes.length, 1) // only n2c2; change n2c1 is not sent diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index ebe65409..dec1236c 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -1,9 +1,8 @@ use crate::exid::ExId; use crate::transaction::{CommitOptions, Transactable}; -use crate::{sync, Keys, KeysAt, ObjType, ScalarValue}; use crate::{ - transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop, - Value, + query, sync, transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, + ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value, }; /// An automerge document that automatically manages transactions. @@ -27,6 +26,11 @@ impl AutoCommit { } } + // FIXME : temp + pub fn actor_to_str(&self, actor: usize) -> String { + self.doc.ops.m.actors.cache[actor].to_hex_string() + } + /// Get the inner document. #[doc(hidden)] pub fn document(&mut self) -> &Automerge { @@ -78,18 +82,18 @@ impl AutoCommit { }) } - pub fn load_incremental(&mut self, data: &[u8]) -> Result { + pub fn load_incremental(&mut self, data: &[u8]) -> Result, AutomergeError> { self.ensure_transaction_closed(); self.doc.load_incremental(data) } - pub fn apply_changes(&mut self, changes: Vec) -> Result<(), AutomergeError> { + pub fn apply_changes(&mut self, changes: Vec) -> Result, AutomergeError> { self.ensure_transaction_closed(); self.doc.apply_changes(changes) } /// Takes all the changes in `other` which are not in `self` and applies them - pub fn merge(&mut self, other: &mut Self) -> Result, AutomergeError> { + pub fn merge(&mut self, other: &mut Self) -> Result, AutomergeError> { self.ensure_transaction_closed(); other.ensure_transaction_closed(); self.doc.merge(&mut other.doc) @@ -149,7 +153,7 @@ impl AutoCommit { &mut self, sync_state: &mut sync::State, message: sync::Message, - ) -> Result<(), AutomergeError> { + ) -> Result, AutomergeError> { self.ensure_transaction_closed(); self.doc.receive_sync_message(sync_state, message) } @@ -285,6 +289,37 @@ impl Transactable for AutoCommit { tx.insert(&mut self.doc, obj.as_ref(), index, value) } + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let tx = self.transaction.as_mut().unwrap(); + tx.mark( + &mut self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { + self.ensure_transaction_open(); + let tx = self.transaction.as_mut().unwrap(); + tx.unmark(&mut self.doc, obj, mark) + } + fn insert_object( &mut self, obj: &ExId, @@ -343,6 +378,44 @@ impl Transactable for AutoCommit { self.doc.text_at(obj, heads) } + fn list>(&self, obj: O) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index b1b9ff1d..a1163b1d 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -13,7 +13,6 @@ use crate::types::{ use crate::KeysAt; use crate::{legacy, query, types, ObjType}; use crate::{AutomergeError, Change, Prop}; -use serde::Serialize; #[derive(Debug, Clone, PartialEq)] pub(crate) enum Actor { @@ -269,7 +268,11 @@ impl Automerge { } pub(crate) fn id_to_exid(&self, id: OpId) -> ExId { - ExId::Id(id.0, self.ops.m.actors.cache[id.1].clone(), id.1) + if id == types::ROOT { + ExId::Root + } else { + ExId::Id(id.0, self.ops.m.actors.cache[id.1].clone(), id.1) + } } /// Get the string represented by the given text object. @@ -305,6 +308,90 @@ impl Automerge { Ok(buffer) } + pub fn list>(&self, obj: O) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let query = self.ops.search(&obj, query::ListVals::new()); + Ok(query + .ops + .iter() + .map(|o| (o.value(), self.id_to_exid(o.id))) + .collect()) + } + + pub fn list_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let clock = self.clock_at(heads); + let query = self.ops.search(&obj, query::ListValsAt::new(clock)); + Ok(query + .ops + .iter() + .map(|o| (o.value(), self.id_to_exid(o.id))) + .collect()) + } + + pub fn spans>(&self, obj: O) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let mut query = self.ops.search(&obj, query::Spans::new()); + query.check_marks(); + Ok(query.spans) + } + + pub fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); + let mut query = self + .ops + .search(&obj, query::Attribute::new(baseline, change_sets)); + query.finish(); + Ok(query.change_sets) + } + + pub fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); + let mut query = self + .ops + .search(&obj, query::Attribute2::new(baseline, change_sets)); + query.finish(); + Ok(query.change_sets) + } + + pub fn raw_spans>( + &self, + obj: O, + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj.as_ref())?; + let query = self.ops.search(&obj, query::RawSpans::new()); + let result = query + .spans + .into_iter() + .map(|s| query::SpanInfo { + id: self.id_to_exid(s.id), + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }) + .collect(); + Ok(result) + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? @@ -409,12 +496,9 @@ impl Automerge { } /// Load an incremental save of a document. - pub fn load_incremental(&mut self, data: &[u8]) -> Result { + pub fn load_incremental(&mut self, data: &[u8]) -> Result, AutomergeError> { let changes = Change::load_document(data)?; - let start = self.ops.len(); - self.apply_changes(changes)?; - let delta = self.ops.len() - start; - Ok(delta) + self.apply_changes(changes) } fn duplicate_seq(&self, change: &Change) -> bool { @@ -428,7 +512,8 @@ impl Automerge { } /// Apply changes to this document. - pub fn apply_changes(&mut self, changes: Vec) -> Result<(), AutomergeError> { + pub fn apply_changes(&mut self, changes: Vec) -> Result, AutomergeError> { + let mut objs = HashSet::new(); for c in changes { if !self.history_index.contains_key(&c.hash) { if self.duplicate_seq(&c) { @@ -438,23 +523,24 @@ impl Automerge { )); } if self.is_causally_ready(&c) { - self.apply_change(c); + self.apply_change(c, &mut objs); } else { self.queue.push(c); } } } while let Some(c) = self.pop_next_causally_ready_change() { - self.apply_change(c); + self.apply_change(c, &mut objs); } - Ok(()) + Ok(objs.into_iter().map(|obj| self.id_to_exid(obj.0)).collect()) } /// Apply a single change to this document. - fn apply_change(&mut self, change: Change) { + fn apply_change(&mut self, change: Change, objs: &mut HashSet) { let ops = self.import_ops(&change); self.update_history(change, ops.len()); for (obj, op) in ops { + objs.insert(obj); self.insert_op(&obj, op); } } @@ -516,15 +602,14 @@ impl Automerge { } /// Takes all the changes in `other` which are not in `self` and applies them - pub fn merge(&mut self, other: &mut Self) -> Result, AutomergeError> { + pub fn merge(&mut self, other: &mut Self) -> Result, AutomergeError> { // TODO: Make this fallible and figure out how to do this transactionally let changes = self .get_changes_added(other) .into_iter() .cloned() .collect::>(); - self.apply_changes(changes)?; - Ok(self.get_heads()) + self.apply_changes(changes) } /// Save the entirety of this document in a compact form. @@ -857,6 +942,8 @@ impl Automerge { OpType::Set(value) => format!("{}", value), OpType::Make(obj) => format!("make({})", obj), OpType::Inc(obj) => format!("inc({})", obj), + OpType::MarkBegin(m) => format!("mark({}={})", m.name, m.value), + OpType::MarkEnd(_) => "/mark".into(), OpType::Del => format!("del{}", 0), }; let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect(); @@ -885,17 +972,6 @@ impl Default for Automerge { } } -#[derive(Serialize, Debug, Clone, PartialEq)] -pub struct SpanInfo { - pub id: ExId, - pub time: i64, - pub start: usize, - pub end: usize, - #[serde(rename = "type")] - pub span_type: String, - pub value: ScalarValue, -} - #[cfg(test)] mod tests { use itertools::Itertools; @@ -1178,8 +1254,7 @@ mod tests { assert!(doc.value_at(&list, 0, &heads2)?.unwrap().0 == Value::int(10)); assert!(doc.length_at(&list, &heads3) == 2); - //doc.dump(); - log!("{:?}", doc.value_at(&list, 0, &heads3)?.unwrap().0); + assert!(doc.value_at(&list, 0, &heads3)?.unwrap().0 == Value::int(30)); assert!(doc.value_at(&list, 1, &heads3)?.unwrap().0 == Value::int(20)); diff --git a/automerge/src/columnar.rs b/automerge/src/columnar.rs index 7cb38872..786dde02 100644 --- a/automerge/src/columnar.rs +++ b/automerge/src/columnar.rs @@ -137,6 +137,15 @@ impl<'a> Iterator for OperationIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Del, Action::Inc => OpType::Inc(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.as_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(amp::Op { action, @@ -178,6 +187,15 @@ impl<'a> Iterator for DocOpIterator<'a> { Action::MakeTable => OpType::Make(ObjType::Table), Action::Del => OpType::Del, Action::Inc => OpType::Inc(value.to_i64()?), + Action::MarkBegin => { + // mark has 3 things in the val column + let name = value.as_string()?; + let expand = self.value.next()?.to_bool()?; + let value = self.value.next()?; + OpType::mark(name, expand, value) + } + Action::MarkEnd => OpType::MarkEnd(value.to_bool()?), + Action::Unused => panic!("invalid action"), }; Some(DocOp { actor, @@ -1063,6 +1081,16 @@ impl DocOpEncoder { self.val.append_null(); Action::Del } + amp::OpType::MarkBegin(m) => { + self.val.append_value(&m.name.clone().into(), actors); + self.val.append_value(&m.expand.into(), actors); + self.val.append_value(&m.value.clone(), actors); + Action::MarkBegin + } + amp::OpType::MarkEnd(s) => { + self.val.append_value(&(*s).into(), actors); + Action::MarkEnd + } amp::OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1169,6 +1197,16 @@ impl ColumnEncoder { self.val.append_null(); Action::Del } + OpType::MarkBegin(m) => { + self.val.append_value2(&m.name.clone().into(), actors); + self.val.append_value2(&m.expand.into(), actors); + self.val.append_value2(&m.value.clone(), actors); + Action::MarkBegin + } + OpType::MarkEnd(s) => { + self.val.append_value2(&(*s).into(), actors); + Action::MarkEnd + } OpType::Make(kind) => { self.val.append_null(); match kind { @@ -1274,8 +1312,11 @@ pub(crate) enum Action { MakeText, Inc, MakeTable, + MarkBegin, + Unused, // final bit is used to mask `Make` actions + MarkEnd, } -const ACTIONS: [Action; 7] = [ +const ACTIONS: [Action; 10] = [ Action::MakeMap, Action::Set, Action::MakeList, @@ -1283,6 +1324,9 @@ const ACTIONS: [Action; 7] = [ Action::MakeText, Action::Inc, Action::MakeTable, + Action::MarkBegin, + Action::Unused, + Action::MarkEnd, ]; impl Decodable for Action { diff --git a/automerge/src/legacy/serde_impls/op.rs b/automerge/src/legacy/serde_impls/op.rs index 1d2a4125..b91ae7e8 100644 --- a/automerge/src/legacy/serde_impls/op.rs +++ b/automerge/src/legacy/serde_impls/op.rs @@ -49,6 +49,12 @@ impl Serialize for Op { match &self.action { OpType::Inc(n) => op.serialize_field("value", &n)?, OpType::Set(value) => op.serialize_field("value", &value)?, + OpType::MarkBegin(m) => { + op.serialize_field("name", &m.name)?; + op.serialize_field("expand", &m.expand)?; + op.serialize_field("value", &m.value)?; + } + OpType::MarkEnd(s) => op.serialize_field("expand", &s)?, _ => {} } op.serialize_field("pred", &self.pred)?; @@ -70,6 +76,8 @@ pub(crate) enum RawOpType { Del, Inc, Set, + MarkBegin, + MarkEnd, } impl Serialize for RawOpType { @@ -85,6 +93,8 @@ impl Serialize for RawOpType { RawOpType::Del => "del", RawOpType::Inc => "inc", RawOpType::Set => "set", + RawOpType::MarkBegin => "mark_begin", + RawOpType::MarkEnd => "mark_end", }; serializer.serialize_str(s) } @@ -116,6 +126,8 @@ impl<'de> Deserialize<'de> for RawOpType { "del" => Ok(RawOpType::Del), "inc" => Ok(RawOpType::Inc), "set" => Ok(RawOpType::Set), + "mark_begin" => Ok(RawOpType::MarkBegin), + "mark_end" => Ok(RawOpType::MarkEnd), other => Err(Error::unknown_variant(other, VARIANTS)), } } @@ -188,6 +200,30 @@ impl<'de> Deserialize<'de> for Op { RawOpType::MakeList => OpType::Make(ObjType::List), RawOpType::MakeText => OpType::Make(ObjType::Text), RawOpType::Del => OpType::Del, + RawOpType::MarkBegin => { + let name = name.ok_or_else(|| Error::missing_field("mark(name)"))?; + let expand = expand.unwrap_or(false); + let value = if let Some(datatype) = datatype { + let raw_value = value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null); + raw_value.as_datatype(datatype).map_err(|e| { + Error::invalid_value( + Unexpected::Other(e.unexpected.as_str()), + &e.expected.as_str(), + ) + })? + } else { + value + .ok_or_else(|| Error::missing_field("value"))? + .unwrap_or(ScalarValue::Null) + }; + OpType::mark(name, expand, value) + } + RawOpType::MarkEnd => { + let expand = expand.unwrap_or(true); + OpType::MarkEnd(expand) + } RawOpType::Set => { let value = if let Some(datatype) = datatype { let raw_value = value diff --git a/automerge/src/legacy/serde_impls/op_type.rs b/automerge/src/legacy/serde_impls/op_type.rs index 19849674..0959b11d 100644 --- a/automerge/src/legacy/serde_impls/op_type.rs +++ b/automerge/src/legacy/serde_impls/op_type.rs @@ -15,6 +15,8 @@ impl Serialize for OpType { OpType::Make(ObjType::Table) => RawOpType::MakeTable, OpType::Make(ObjType::List) => RawOpType::MakeList, OpType::Make(ObjType::Text) => RawOpType::MakeText, + OpType::MarkBegin(_) => RawOpType::MarkBegin, + OpType::MarkEnd(_) => RawOpType::MarkEnd, OpType::Del => RawOpType::Del, OpType::Inc(_) => RawOpType::Inc, OpType::Set(_) => RawOpType::Set, diff --git a/automerge/src/op_set.rs b/automerge/src/op_set.rs index 840d6617..6859df04 100644 --- a/automerge/src/op_set.rs +++ b/automerge/src/op_set.rs @@ -90,10 +90,6 @@ impl OpSetInternal { op } - pub fn len(&self) -> usize { - self.length - } - pub fn insert(&mut self, index: usize, obj: &ObjId, element: Op) { if let OpType::Make(typ) = element.action { self.trees diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 7732e908..f413d590 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -1,10 +1,14 @@ +use crate::exid::ExId; use crate::op_tree::{OpSetMetadata, OpTreeNode}; use crate::types::{Clock, Counter, ElemId, Op, OpId, OpType, ScalarValue}; use fxhash::FxBuildHasher; +use serde::Serialize; use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +mod attribute; +mod attribute2; mod insert; mod keys; mod keys_at; @@ -17,8 +21,12 @@ mod nth_at; mod opid; mod prop; mod prop_at; +mod raw_spans; mod seek_op; +mod spans; +pub(crate) use attribute::{Attribute, ChangeSet}; +pub(crate) use attribute2::{Attribute2, ChangeSet2}; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; pub(crate) use keys_at::KeysAt; @@ -31,7 +39,19 @@ pub(crate) use nth_at::NthAt; pub(crate) use opid::OpIdSearch; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; +pub(crate) use raw_spans::RawSpans; pub(crate) use seek_op::SeekOp; +pub(crate) use spans::{Span, Spans}; + +#[derive(Serialize, Debug, Clone, PartialEq)] +pub struct SpanInfo { + pub id: ExId, + pub start: usize, + pub end: usize, + #[serde(rename = "type")] + pub span_type: String, + pub value: ScalarValue, +} #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { diff --git a/automerge/src/query/attribute.rs b/automerge/src/query/attribute.rs new file mode 100644 index 00000000..72415483 --- /dev/null +++ b/automerge/src/query/attribute.rs @@ -0,0 +1,128 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet { + clock: Clock, + next_add: Option>, + next_del: Option<(usize, String)>, + pub add: Vec>, + pub del: Vec<(usize, String)>, +} + +impl From for ChangeSet { + fn from(clock: Clock) -> Self { + ChangeSet { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute { + pub fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(range) = &mut cs.next_add { + range.end += 1; + } else { + cs.next_add = Some(Range { + start: self.seen, + end: self.seen + 1, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if baseline && element.succ.iter().any(|id| cs.clock.covers(id)) { + // was deleted by change set + if let Some(s) = element.as_string() { + if let Some((_, span)) = &mut cs.next_del { + span.push_str(&s); + } else { + cs.next_del = Some((self.seen, s)) + } + } + } else { + //cs.cut_del(); + } + //cs.cut_add(); + } + } + + pub fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl TreeQuery for Attribute { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/attribute2.rs b/automerge/src/query/attribute2.rs new file mode 100644 index 00000000..02f158c6 --- /dev/null +++ b/automerge/src/query/attribute2.rs @@ -0,0 +1,172 @@ +use crate::clock::Clock; +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op}; +use std::fmt::Debug; +use std::ops::Range; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Attribute2 { + pos: usize, + seen: usize, + last_seen: Option, + baseline: Clock, + pub change_sets: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ChangeSet2 { + clock: Clock, + next_add: Option, + next_del: Option, + pub add: Vec, + pub del: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Add { + pub actor: usize, + pub range: Range, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CS2Del { + pub pos: usize, + pub actor: usize, + pub span: String, +} + +impl From for ChangeSet2 { + fn from(clock: Clock) -> Self { + ChangeSet2 { + clock, + next_add: None, + next_del: None, + add: Vec::new(), + del: Vec::new(), + } + } +} + +impl ChangeSet2 { + fn cut_add(&mut self) { + if let Some(add) = self.next_add.take() { + self.add.push(add) + } + } + + fn cut_del(&mut self) { + if let Some(del) = self.next_del.take() { + self.del.push(del) + } + } +} + +impl Attribute2 { + pub fn new(baseline: Clock, change_sets: Vec) -> Self { + Attribute2 { + pos: 0, + seen: 0, + last_seen: None, + baseline, + change_sets: change_sets.into_iter().map(|c| c.into()).collect(), + } + } + + fn update_add(&mut self, element: &Op) { + let baseline = self.baseline.covers(&element.id); + for cs in &mut self.change_sets { + if !baseline && cs.clock.covers(&element.id) { + // is part of the change_set + if let Some(CS2Add { range, actor }) = &mut cs.next_add { + if *actor == element.id.actor() { + range.end += 1; + } else { + cs.cut_add(); + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.next_add = Some(CS2Add { + actor: element.id.actor(), + range: Range { + start: self.seen, + end: self.seen + 1, + }, + }); + } + } else { + cs.cut_add(); + } + cs.cut_del(); + } + } + + // id is in baseline + // succ is not in baseline but is in cs + + fn update_del(&mut self, element: &Op) { + if !self.baseline.covers(&element.id) { + return; + } + for cs in &mut self.change_sets { + let succ: Vec<_> = element + .succ + .iter() + .filter(|id| cs.clock.covers(id)) + .collect(); + // was deleted by change set + if let Some(suc) = succ.get(0) { + if let Some(s) = element.as_string() { + if let Some(CS2Del { actor, span, .. }) = &mut cs.next_del { + if suc.actor() == *actor { + span.push_str(&s); + } else { + cs.cut_del(); + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s, + }) + } + } else { + cs.next_del = Some(CS2Del { + pos: self.seen, + actor: suc.actor(), + span: s, + }) + } + } + } + } + } + + pub fn finish(&mut self) { + for cs in &mut self.change_sets { + cs.cut_add(); + cs.cut_del(); + } + } +} + +impl TreeQuery for Attribute2 { + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { + if element.insert { + self.last_seen = None; + } + if self.last_seen.is_none() && element.visible() { + self.update_add(element); + self.seen += 1; + self.last_seen = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/insert.rs b/automerge/src/query/insert.rs index eb855ce9..34ee4059 100644 --- a/automerge/src/query/insert.rs +++ b/automerge/src/query/insert.rs @@ -99,6 +99,10 @@ impl TreeQuery for InsertNth { self.last_seen = None; self.last_insert = element.elemid(); } + if self.valid.is_some() && element.valid_mark_anchor() { + self.last_valid_insert = element.elemid(); + self.valid = None; + } if self.last_seen.is_none() && element.visible() { if self.seen >= self.target { return QueryResult::Finish; diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs new file mode 100644 index 00000000..e375e683 --- /dev/null +++ b/automerge/src/query/raw_spans.rs @@ -0,0 +1,78 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpId, OpType, ScalarValue}; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpans { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + changed: bool, + pub spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct RawSpan { + pub id: OpId, + pub start: usize, + pub end: usize, + pub name: String, + pub value: ScalarValue, +} + +impl RawSpans { + pub fn new() -> Self { + RawSpans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + changed: false, + spans: Vec::new(), + } + } +} + +impl TreeQuery for RawSpans { + fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(md) = &element.action { + let pos = self + .spans + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.spans.insert( + pos, + RawSpan { + id: element.id, + start: self.seen, + end: 0, + name: md.name.clone(), + value: md.value.clone(), + }, + ); + } + if let OpType::MarkEnd(_) = &element.action { + for s in self.spans.iter_mut() { + if s.id == element.id.prev() { + s.end = self.seen; + break; + } + } + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.seen += 1; + self.last_seen = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/query/spans.rs b/automerge/src/query/spans.rs new file mode 100644 index 00000000..589dba03 --- /dev/null +++ b/automerge/src/query/spans.rs @@ -0,0 +1,108 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpType, ScalarValue}; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Spans { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec, + marks: HashMap, + changed: bool, + pub spans: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Span { + pub pos: usize, + pub marks: Vec<(String, ScalarValue)>, +} + +impl Spans { + pub fn new() -> Self { + Spans { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + seen_at_last_mark: None, + seen_at_this_mark: None, + changed: false, + ops: Vec::new(), + marks: HashMap::new(), + spans: Vec::new(), + } + } + + pub fn check_marks(&mut self) { + let mut new_marks = HashMap::new(); + for op in &self.ops { + if let OpType::MarkBegin(m) = &op.action { + new_marks.insert(m.name.clone(), m.value.clone()); + } + } + if new_marks != self.marks { + self.changed = true; + self.marks = new_marks; + } + if self.changed + && (self.seen_at_last_mark != self.seen_at_this_mark + || self.seen_at_last_mark.is_none() && self.seen_at_this_mark.is_none()) + { + self.changed = false; + self.seen_at_last_mark = self.seen_at_this_mark; + let mut marks: Vec<_> = self + .marks + .iter() + .map(|(key, val)| (key.clone(), val.clone())) + .collect(); + marks.sort_by(|(k1, _), (k2, _)| k1.cmp(k2)); + self.spans.push(Span { + pos: self.seen, + marks, + }); + } + } +} + +impl TreeQuery for Spans { + /* + fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { + unimplemented!() + } + */ + + fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { + // find location to insert + // mark or set + if element.succ.is_empty() { + if let OpType::MarkBegin(_) = &element.action { + let pos = self + .ops + .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) + .unwrap_err(); + self.ops.insert(pos, element.clone()); + } + if let OpType::MarkEnd(_) = &element.action { + self.ops.retain(|op| op.id != element.id.prev()); + } + } + if element.insert { + self.last_seen = None; + self.last_insert = element.elemid(); + } + if self.last_seen.is_none() && element.visible() { + self.check_marks(); + self.seen += 1; + self.last_seen = element.elemid(); + self.seen_at_this_mark = element.elemid(); + } + self.pos += 1; + QueryResult::Next + } +} diff --git a/automerge/src/sync.rs b/automerge/src/sync.rs index 43801b9c..da9bcc73 100644 --- a/automerge/src/sync.rs +++ b/automerge/src/sync.rs @@ -1,3 +1,7 @@ +use crate::exid::ExId; +use crate::{ + decoding, decoding::Decoder, encoding::Encodable, Automerge, AutomergeError, Change, ChangeHash, +}; use itertools::Itertools; use std::{ borrow::Cow, @@ -6,10 +10,6 @@ use std::{ io::Write, }; -use crate::{ - decoding, decoding::Decoder, encoding::Encodable, Automerge, AutomergeError, Change, ChangeHash, -}; - mod bloom; mod state; @@ -97,7 +97,8 @@ impl Automerge { &mut self, sync_state: &mut State, message: Message, - ) -> Result<(), AutomergeError> { + ) -> Result, AutomergeError> { + let mut result = vec![]; let before_heads = self.get_heads(); let Message { @@ -109,7 +110,7 @@ impl Automerge { let changes_is_empty = message_changes.is_empty(); if !changes_is_empty { - self.apply_changes(message_changes)?; + result = self.apply_changes(message_changes)?; sync_state.shared_heads = advance_heads( &before_heads.iter().collect(), &self.get_heads().into_iter().collect(), @@ -150,7 +151,7 @@ impl Automerge { sync_state.their_heads = Some(message_heads); sync_state.their_need = Some(message_need); - Ok(()) + Ok(result) } fn make_bloom_filter(&self, last_sync: Vec) -> Have { diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index c471c057..04c62f18 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -152,6 +152,71 @@ impl TransactionInner { self.operations.push((obj, op)); } + #[allow(clippy::too_many_arguments)] + pub fn mark>( + &mut self, + doc: &mut Automerge, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj.as_ref())?; + + self.do_insert( + doc, + obj, + start, + OpType::mark(mark.into(), expand_start, value), + )?; + self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?; + + Ok(()) + } + + pub fn unmark>( + &mut self, + doc: &mut Automerge, + obj: O, + mark: O, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj.as_ref())?; + let markid = doc.exid_to_obj(mark.as_ref())?.0; + let op1 = Op { + id: self.next_id(), + action: OpType::Del, + key: markid.into(), + succ: Default::default(), + pred: vec![markid], + insert: false, + }; + let q1 = doc.ops.search(&obj, query::SeekOp::new(&op1)); + for i in q1.succ { + doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op1)); + } + self.operations.push((obj, op1)); + + let markid = markid.next(); + let op2 = Op { + id: self.next_id(), + action: OpType::Del, + key: markid.into(), + succ: Default::default(), + pred: vec![markid], + insert: false, + }; + let q2 = doc.ops.search(&obj, query::SeekOp::new(&op2)); + + for i in q2.succ { + doc.ops.replace(&obj, i, |old_op| old_op.add_succ(&op2)); + } + self.operations.push((obj, op2)); + Ok(()) + } + pub fn insert>( &mut self, doc: &mut Automerge, @@ -189,6 +254,7 @@ impl TransactionInner { let query = doc.ops.search(&obj, query::InsertNth::new(index)); let key = query.key()?; + let is_make = matches!(&action, OpType::Make(_)); let op = Op { diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index 2303bb34..cdfd7530 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -1,6 +1,6 @@ use crate::exid::ExId; -use crate::{Automerge, ChangeHash, KeysAt, ObjType, Prop, ScalarValue, Value}; -use crate::{AutomergeError, Keys}; +use crate::AutomergeError; +use crate::{query, Automerge, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value}; use super::{CommitOptions, Transactable, TransactionInner}; @@ -121,6 +121,33 @@ impl<'a> Transactable for Transaction<'a> { .insert(self.doc, obj.as_ref(), index, value) } + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + self.inner.as_mut().unwrap().mark( + self.doc, + obj, + start, + expand_start, + end, + expand_end, + mark, + value, + ) + } + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError> { + self.inner.as_mut().unwrap().unmark(self.doc, obj, mark) + } + fn insert_object( &mut self, obj: &ExId, @@ -203,6 +230,44 @@ impl<'a> Transactable for Transaction<'a> { self.doc.text_at(obj, heads) } + fn list>(&self, obj: O) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute(obj, baseline, change_sets) + } + + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.attribute2(obj, baseline, change_sets) + } + fn value, P: Into>( &self, obj: O, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index 68852180..fa8e7ac9 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -1,4 +1,5 @@ use crate::exid::ExId; +use crate::query; use crate::{AutomergeError, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value}; use unicode_segmentation::UnicodeSegmentation; @@ -57,6 +58,21 @@ pub trait Transactable { object: ObjType, ) -> Result; + /// Set a mark within a range on a list + #[allow(clippy::too_many_arguments)] + fn mark>( + &mut self, + obj: O, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError>; + + fn unmark>(&mut self, obj: O, mark: O) -> Result<(), AutomergeError>; + /// Increment the counter at the prop in the object by `value`. fn inc, P: Into>( &mut self, @@ -115,6 +131,38 @@ pub trait Transactable { heads: &[ChangeHash], ) -> Result; + /// Get the string that this text object represents. + fn list>(&self, obj: O) -> Result, AutomergeError>; + + /// Get the string that this text object represents at a point in history. + fn list_at>( + &self, + obj: O, + heads: &[ChangeHash], + ) -> Result, AutomergeError>; + + /// test spans api for mark/span experiment + fn spans>(&self, obj: O) -> Result, AutomergeError>; + + /// test raw_spans api for mark/span experiment + fn raw_spans>(&self, obj: O) -> Result, AutomergeError>; + + /// test attribute api for mark/span experiment + fn attribute>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + + /// test attribute api for mark/span experiment + fn attribute2>( + &self, + obj: O, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + /// Get the value at this prop in the object. fn value, P: Into>( &self, diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 557594c8..ae4ed432 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -171,6 +171,25 @@ pub enum OpType { Del, Inc(i64), Set(ScalarValue), + MarkBegin(MarkData), + MarkEnd(bool), +} + +impl OpType { + pub(crate) fn mark(name: String, expand: bool, value: ScalarValue) -> Self { + OpType::MarkBegin(MarkData { + name, + expand, + value, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct MarkData { + pub name: String, + pub value: ScalarValue, + pub expand: bool, } impl From for OpType { @@ -205,6 +224,14 @@ impl OpId { pub fn actor(&self) -> usize { self.1 } + #[inline] + pub fn prev(&self) -> OpId { + OpId(self.0 - 1, self.1) + } + #[inline] + pub fn next(&self) -> OpId { + OpId(self.0 + 1, self.1) + } } impl Exportable for ObjId { @@ -396,7 +423,7 @@ impl Op { } pub fn visible(&self) -> bool { - if self.is_inc() { + if self.is_inc() || self.is_mark() { false } else if self.is_counter() { self.succ.len() <= self.incs() @@ -421,6 +448,18 @@ impl Op { matches!(&self.action, OpType::Inc(_)) } + pub fn valid_mark_anchor(&self) -> bool { + self.succ.is_empty() + && matches!( + &self.action, + OpType::MarkBegin(MarkData { expand: true, .. }) | OpType::MarkEnd(false) + ) + } + + pub fn is_mark(&self) -> bool { + matches!(&self.action, OpType::MarkBegin(_) | OpType::MarkEnd(_)) + } + pub fn is_counter(&self) -> bool { matches!(&self.action, OpType::Set(ScalarValue::Counter(_))) } @@ -441,6 +480,13 @@ impl Op { } } + pub fn as_string(&self) -> Option { + match &self.action { + OpType::Set(scalar) => scalar.as_string(), + _ => None, + } + } + pub fn value(&self) -> Value { match &self.action { OpType::Make(obj_type) => Value::Object(*obj_type), @@ -455,6 +501,8 @@ impl Op { OpType::Set(value) if self.insert => format!("i:{}", value), OpType::Set(value) => format!("s:{}", value), OpType::Make(obj) => format!("make{}", obj), + OpType::MarkBegin(m) => format!("mark{}={}", m.name, m.value), + OpType::MarkEnd(_) => "unmark".into(), OpType::Inc(val) => format!("inc:{}", val), OpType::Del => "del".to_string(), } diff --git a/automerge/src/value.rs b/automerge/src/value.rs index f837ad63..c28d9259 100644 --- a/automerge/src/value.rs +++ b/automerge/src/value.rs @@ -11,6 +11,13 @@ pub enum Value { } impl Value { + pub fn as_string(&self) -> Option { + match self { + Value::Scalar(val) => val.as_string(), + _ => None, + } + } + pub fn map() -> Value { Value::Object(ObjType::Map) } @@ -591,6 +598,13 @@ impl ScalarValue { } } + pub fn as_string(&self) -> Option { + match self { + ScalarValue::Str(s) => Some(s.to_string()), + _ => None, + } + } + pub fn counter(n: i64) -> ScalarValue { ScalarValue::Counter(n.into()) } diff --git a/automerge/src/visualisation.rs b/automerge/src/visualisation.rs index 6f6a36b0..a25bf22a 100644 --- a/automerge/src/visualisation.rs +++ b/automerge/src/visualisation.rs @@ -238,6 +238,8 @@ impl OpTableRow { crate::OpType::Set(v) => format!("set {}", v), crate::OpType::Make(obj) => format!("make {}", obj), crate::OpType::Inc(v) => format!("inc {}", v), + crate::OpType::MarkBegin(v) => format!("mark {}={}", v.name, v.value), + crate::OpType::MarkEnd(v) => format!("/mark {}", v), }; let prop = match op.key { crate::types::Key::Map(k) => metadata.props[k].clone(), diff --git a/automerge/tests/attribute.rs b/automerge/tests/attribute.rs new file mode 100644 index 00000000..c2996656 --- /dev/null +++ b/automerge/tests/attribute.rs @@ -0,0 +1,39 @@ +use automerge::transaction::Transactable; +use automerge::{AutoCommit, AutomergeError, ROOT}; + +/* +mod helpers; +use helpers::{ + pretty_print, realize, realize_obj, + RealizedObject, +}; +*/ + +#[test] +fn simple_attribute_text() -> Result<(), AutomergeError> { + let mut doc = AutoCommit::new(); + let note = doc.set_object(&ROOT, "note", automerge::ObjType::Text)?; + doc.splice_text(¬e, 0, 0, "hello little world")?; + let baseline = doc.get_heads(); + assert!(doc.text(¬e).unwrap() == "hello little world"); + let mut doc2 = doc.fork(); + doc2.splice_text(¬e, 5, 7, " big")?; + let h2 = doc2.get_heads(); + assert!(doc2.text(¬e)? == "hello big world"); + let mut doc3 = doc.fork(); + doc3.splice_text(¬e, 0, 0, "Well, ")?; + let h3 = doc3.get_heads(); + assert!(doc3.text(¬e)? == "Well, hello little world"); + doc.merge(&mut doc2)?; + doc.merge(&mut doc3)?; + let text = doc.text(¬e)?; + assert!(text == "Well, hello big world"); + let cs = vec![h2, h3]; + let attribute = doc.attribute(¬e, &baseline, &cs)?; + assert!(&text[attribute[0].add[0].clone()] == " big"); + assert!(attribute[0].del[0] == (15, " little".to_owned())); + //println!("{:?} == {:?}", attribute[0].del[0] , (15, " little".to_owned())); + assert!(&text[attribute[1].add[0].clone()] == "Well, "); + //println!("- ------- attribute = {:?}", attribute); + Ok(()) +} diff --git a/scripts/ci/js_tests b/scripts/ci/js_tests index 9b1d0e77..6c4a16d4 100755 --- a/scripts/ci/js_tests +++ b/scripts/ci/js_tests @@ -7,9 +7,9 @@ yarn --cwd $WASM_PROJECT install; yarn --cwd $WASM_PROJECT build; # If the dependencies are already installed we delete automerge-wasm. This makes # this script usable for iterative development. -if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then - rm -rf $JS_PROJECT/node_modules/automerge-wasm -fi +#if [ -d $JS_PROJECT/node_modules/automerge-wasm ]; then +# rm -rf $JS_PROJECT/node_modules/automerge-wasm +#fi # --check-files forces yarn to check if the local dep has changed yarn --cwd $JS_PROJECT install --check-files; yarn --cwd $JS_PROJECT test;