From 561cad44e3aeee4e84d36345b3d79b8349d6a7a8 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 11:42:58 -0500 Subject: [PATCH 01/45] Revert "remove marks" This reverts commit c8c695618b678b63aac31364cf4203d7a6f507b2. --- automerge-wasm/index.d.ts | 5 + automerge-wasm/src/lib.rs | 73 +++++++++++ automerge-wasm/test/marks.ts | 138 ++++++++++++++++++++ automerge/src/automerge.rs | 88 ++++++++++++- automerge/src/columnar.rs | 46 ++++++- automerge/src/legacy/serde_impls/op.rs | 36 +++++ automerge/src/legacy/serde_impls/op_type.rs | 2 + automerge/src/query.rs | 4 + automerge/src/query/insert.rs | 4 + automerge/src/query/raw_spans.rs | 71 ++++++++++ automerge/src/query/spans.rs | 108 +++++++++++++++ automerge/src/types.rs | 39 +++++- automerge/tests/test.rs | 96 +++++++------- 13 files changed, 658 insertions(+), 52 deletions(-) create mode 100644 automerge-wasm/test/marks.ts create mode 100644 automerge/src/query/raw_spans.rs create mode 100644 automerge/src/query/spans.rs diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 20189dab..8a7e9408 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -101,6 +101,11 @@ export class Automerge { text(obj: ObjID, heads?: Heads): string; length(obj: ObjID, heads?: Heads): number; + // experimental spans api - unstable! + mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; + spans(obj: ObjID): any; + raw_spans(obj: ObjID): any; + // transactions commit(message?: string, time?: number): Heads; merge(other: Automerge): Heads; diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index b4973f6f..b2f8e277 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -2,6 +2,7 @@ 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; @@ -305,6 +306,78 @@ impl Automerge { Ok(()) } + pub fn mark( + &mut self, + obj: String, + 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 spans(&mut self, obj: String) -> 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.is_empty() { + 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.is_empty() { + result.push(&text_span.into()); + } + Ok(result.into()) + } + + pub fn raw_spans(&mut self, obj: String) -> 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 save(&mut self) -> Result { self.0 .save() diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts new file mode 100644 index 00000000..61951056 --- /dev/null +++ b/automerge-wasm/test/marks.ts @@ -0,0 +1,138 @@ +import { describe, it } from 'mocha'; +//@ts-ignore +import assert from 'assert' +//@ts-ignore +import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '../dev/index' + +describe('Automerge', () => { + describe('marks', () => { + it('should handle marks [..]', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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 with deleted ends [..]', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + + 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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",999); + 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", time: 999, start: 0, end: 37, type: 'bold', value: true }, + { id: "41@aabbcc", time: 999, start: 4, end: 19, type: 'itallic', value: true }, + { id: "43@aabbcc", time: 999, start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + ]); + + // mark sure encode decode can handle marks + + let all = doc.getChanges([]) + let decoded = all.map((c) => decodeChange(c)) + let encoded = decoded.map((c) => encodeChange(c)) + let doc2 = create(); + doc2.applyChanges(encoded) + + assert.deepStrictEqual(doc.spans(list) , doc2.spans(list)) + assert.deepStrictEqual(doc.save(), doc2.save()) + }) + }) +}) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index c3918354..122fa884 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -453,6 +453,90 @@ impl Automerge { Ok(buffer) } + pub fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let mut query = self.ops.search(obj, query::Spans::new()); + query.check_marks(); + Ok(query.spans) + } + + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let query = self.ops.search(obj, query::RawSpans::new()); + let result = query.spans.into_iter().map(|s| SpanInfo { + id: self.id_to_exid(s.id), + time: self.history[s.change].time, + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }).collect(); + Ok(result) + } + + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = self.exid_to_obj(obj)?; + + self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; + self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; + + /* + let (a, b) = query.ops()?; + let (pos, key) = a; + let id = self.next_id(); + let op = Op { + change: self.history.len(), + id, + action: OpType::Mark(MarkData { name: mark.into(), expand: expand_start, value}), + obj, + key, + succ: Default::default(), + pred: Default::default(), + insert: true, + }; + self.ops.insert(pos, op.clone()); + self.tx().operations.push(op); + + let (pos, key) = b; + let id = self.next_id(); + let op = Op { + change: self.history.len(), + id, + action: OpType::Unmark(expand_end), + obj, + key, + succ: Default::default(), + pred: Default::default(), + insert: true, + }; + self.ops.insert(pos, op.clone()); + self.tx().operations.push(op); + */ + + Ok(()) + } + + pub fn unmark( + &self, + _obj: &ExId, + _start: usize, + _end: usize, + _inclusive: bool, + _mark: &str, + ) -> Result { + unimplemented!() + } + // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } // Something better? @@ -1114,6 +1198,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<_> = i.pred.iter().map(|id| self.to_string(*id)).collect(); @@ -1384,8 +1470,6 @@ 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 53a9d488..28aca822 100644 --- a/automerge/src/columnar.rs +++ b/automerge/src/columnar.rs @@ -134,6 +134,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.to_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, @@ -175,6 +184,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.to_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, @@ -1064,6 +1082,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 { @@ -1170,6 +1198,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 { @@ -1275,8 +1313,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, @@ -1284,6 +1325,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/query.rs b/automerge/src/query.rs index 7911e1bb..ff97532e 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -17,6 +17,8 @@ mod nth_at; mod prop; mod prop_at; mod seek_op; +mod spans; +mod raw_spans; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -30,6 +32,8 @@ pub(crate) use nth_at::NthAt; pub(crate) use prop::Prop; pub(crate) use prop_at::PropAt; pub(crate) use seek_op::SeekOp; +pub(crate) use spans::{Span, Spans}; +pub(crate) use raw_spans::RawSpans; #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { diff --git a/automerge/src/query/insert.rs b/automerge/src/query/insert.rs index 62da48f9..38a58e45 100644 --- a/automerge/src/query/insert.rs +++ b/automerge/src/query/insert.rs @@ -85,6 +85,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..77a45741 --- /dev/null +++ b/automerge/src/query/raw_spans.rs @@ -0,0 +1,71 @@ +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 change: usize, + 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, change: element.change, 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/types.rs b/automerge/src/types.rs index 4494f6d9..54192908 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -158,6 +158,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, } #[derive(Debug)] @@ -180,6 +199,10 @@ impl OpId { pub fn actor(&self) -> usize { self.1 } + #[inline] + pub fn prev(&self) -> OpId { + OpId(self.0 - 1, self.1) + } } impl Exportable for ObjId { @@ -376,7 +399,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() @@ -401,6 +424,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(_))) } @@ -435,6 +470,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/tests/test.rs b/automerge/tests/test.rs index 03d5a5d2..d8637283 100644 --- a/automerge/tests/test.rs +++ b/automerge/tests/test.rs @@ -54,10 +54,10 @@ fn repeated_map_assignment_which_resolves_conflict_not_ignored() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "field", 123).unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&automerge::ROOT, "field", 456).unwrap(); doc1.set(&automerge::ROOT, "field", 789).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_eq!(doc1.values(&automerge::ROOT, "field").unwrap().len(), 2); doc1.set(&automerge::ROOT, "field", 123).unwrap(); @@ -78,9 +78,9 @@ fn repeated_list_assignment_which_resolves_conflict_not_ignored() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, 123).unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&list_id, 0, 456).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc1.set(&list_id, 0, 789).unwrap(); assert_doc!( @@ -123,7 +123,7 @@ fn merge_concurrent_map_prop_updates() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "foo", "bar").unwrap(); doc2.set(&automerge::ROOT, "hello", "world").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_eq!( doc1.value(&automerge::ROOT, "foo").unwrap().unwrap().0, "bar".into() @@ -135,7 +135,7 @@ fn merge_concurrent_map_prop_updates() { "hello" => { "world" }, } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -152,10 +152,10 @@ fn add_concurrent_increments_of_same_property() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "counter", mk_counter(0)) .unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.inc(&automerge::ROOT, "counter", 1).unwrap(); doc2.inc(&automerge::ROOT, "counter", 2).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, map! { @@ -181,7 +181,7 @@ fn add_increments_only_to_preceeded_values() { doc2.inc(&automerge::ROOT, "counter", 3).unwrap(); // The two values should be conflicting rather than added - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -201,7 +201,7 @@ fn concurrent_updates_of_same_field() { doc1.set(&automerge::ROOT, "field", "one").unwrap(); doc2.set(&automerge::ROOT, "field", "two").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -223,11 +223,11 @@ fn concurrent_updates_of_same_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "finch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&list_id, 0, "greenfinch").unwrap(); doc2.set(&list_id, 0, "goldfinch").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -252,8 +252,8 @@ fn assignment_conflicts_of_different_types() { .unwrap(); doc3.set(&automerge::ROOT, "field", automerge::Value::map()) .unwrap(); - doc1.merge(&mut doc2).unwrap(); - doc1.merge(&mut doc3).unwrap(); + doc1.merge(&mut doc2); + doc1.merge(&mut doc3); assert_doc!( &doc1, @@ -277,7 +277,7 @@ fn changes_within_conflicting_map_field() { .unwrap() .unwrap(); doc2.set(&map_id, "innerKey", 42).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -304,7 +304,7 @@ fn changes_within_conflicting_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "hello").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); let map_in_doc1 = doc1 .set(&list_id, 0, automerge::Value::map()) @@ -317,11 +317,11 @@ fn changes_within_conflicting_list_element() { .set(&list_id, 0, automerge::Value::map()) .unwrap() .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc2.set(&map_in_doc2, "map2", true).unwrap(); doc2.set(&map_in_doc2, "key", 2).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -361,7 +361,7 @@ fn concurrently_assigned_nested_maps_should_not_merge() { .unwrap(); doc2.set(&doc2_map_id, "logo_url", "logo.png").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -392,11 +392,11 @@ fn concurrent_insertions_at_different_list_positions() { doc1.insert(&list_id, 0, "one").unwrap(); doc1.insert(&list_id, 1, "three").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice(&list_id, 1, 0, vec!["two".into()]).unwrap(); doc2.insert(&list_id, 2, "four").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -426,10 +426,10 @@ fn concurrent_insertions_at_same_list_position() { .unwrap(); doc1.insert(&list_id, 0, "parakeet").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.insert(&list_id, 1, "starling").unwrap(); doc2.insert(&list_id, 1, "chaffinch").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -456,11 +456,11 @@ fn concurrent_assignment_and_deletion_of_a_map_entry() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "bestBird", "robin").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&automerge::ROOT, "bestBird").unwrap(); doc2.set(&automerge::ROOT, "bestBird", "magpie").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -483,7 +483,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { doc1.insert(&list_id, 0, "blackbird").unwrap(); doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&list_id, 1, "starling").unwrap(); doc2.del(&list_id, 1).unwrap(); @@ -508,7 +508,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { } ); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -535,14 +535,14 @@ fn insertion_after_a_deleted_list_element() { doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice(&list_id, 1, 2, Vec::new()).unwrap(); doc2.splice(&list_id, 2, 0, vec!["starling".into()]) .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -554,7 +554,7 @@ fn insertion_after_a_deleted_list_element() { } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -579,13 +579,13 @@ fn concurrent_deletion_of_same_list_element() { doc1.insert(&list_id, 1, "buzzard").unwrap(); doc1.insert(&list_id, 2, "cormorant").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&list_id, 1).unwrap(); doc2.del(&list_id, 1).unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -597,7 +597,7 @@ fn concurrent_deletion_of_same_list_element() { } ); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); assert_doc!( &doc2, map! { @@ -631,12 +631,12 @@ fn concurrent_updates_at_different_levels() { .unwrap(); doc1.insert(&mammals, 0, "badger").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.set(&birds, "brown", "sparrow").unwrap(); doc2.del(&animals, "birds").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_obj!( &doc1, @@ -676,13 +676,13 @@ fn concurrent_updates_of_concurrently_deleted_objects() { .unwrap(); doc1.set(&blackbird, "feathers", "black").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.del(&birds, "blackbird").unwrap(); doc2.set(&blackbird, "beak", "orange").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -704,7 +704,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { .set(&automerge::ROOT, "wisdom", automerge::Value::list()) .unwrap() .unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc1.splice( &wisdom, @@ -734,7 +734,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { ) .unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); assert_doc!( &doc1, @@ -767,7 +767,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_greater_actor_id( .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -793,7 +793,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_lesser_actor_id() .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -817,11 +817,11 @@ fn insertion_consistent_with_causality() { .unwrap() .unwrap(); doc1.insert(&list, 0, "four").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "three").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -861,11 +861,11 @@ fn save_restore_complex() { doc1.set(&first_todo, "done", false).unwrap(); let mut doc2 = new_doc(); - doc2.merge(&mut doc1).unwrap(); + doc2.merge(&mut doc1); doc2.set(&first_todo, "title", "weed plants").unwrap(); doc1.set(&first_todo, "title", "kill plants").unwrap(); - doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc2); let reloaded = Automerge::load(&doc1.save().unwrap()).unwrap(); @@ -918,8 +918,8 @@ fn list_counter_del() -> Result<(), automerge::AutomergeError> { doc1.inc(&list, 1, 1)?; doc1.inc(&list, 2, 1)?; - doc1.merge(&mut doc2).unwrap(); - doc1.merge(&mut doc3).unwrap(); + doc1.merge(&mut doc2); + doc1.merge(&mut doc3); let values = doc1.values(&list, 1)?; assert_eq!(values.len(), 3); From 2ba2da95a8b03d473036d697c2102f1ed2c0f000 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 14:41:57 -0500 Subject: [PATCH 02/45] attempt at new packaging --- automerge-wasm/.gitignore | 2 + automerge-wasm/Cargo.toml | 6 +- automerge-wasm/README.md | 689 ----------------------------------- automerge-wasm/package.json | 24 +- automerge-wasm/test/marks.ts | 2 +- automerge-wasm/test/test.ts | 6 +- 6 files changed, 23 insertions(+), 706 deletions(-) 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 8225d811..5bc82cd7 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 985955a3..bee84123 100644 --- a/automerge-wasm/README.md +++ b/automerge-wasm/README.md @@ -2,692 +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 - -#[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/package.json b/automerge-wasm/package.json index c39299eb..9fa9bbf2 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -4,25 +4,29 @@ "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.5", "license": "MIT", "files": [ "README.md", "LICENSE", "package.json", - "automerge_wasm_bg.wasm", + "types.d.ts", + "node/index.js", + "node/index_bg.wasm", + "web/index.js", + "web/index_bg.wasm", "automerge_wasm.js" ], - "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 && yarn opt && 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", - "opt": "wasm-opt -Oz dev/index_bg.wasm -o tmp.wasm && mv tmp.wasm dev/index_bg.wasm", + "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/test/marks.ts b/automerge-wasm/test/marks.ts index 61951056..ab08636f 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -2,7 +2,7 @@ import { describe, it } from 'mocha'; //@ts-ignore import assert from 'assert' //@ts-ignore -import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '../dev/index' +import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '..' describe('Automerge', () => { describe('marks', () => { diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index f72d0979..51e50789 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,9 +3,9 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index' -import { DecodedSyncMessage } from '../index'; -import { Hash } from '../dev/index'; +import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { DecodedSyncMessage } from '..'; +import { Hash } from '..'; function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) { const MAX_ITER = 10 From c8cd069e51bf655d022aab1cb452194b6f90fd0e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 19:15:02 -0500 Subject: [PATCH 03/45] tweak files --- automerge-wasm/package.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 9fa9bbf2..e74cf803 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -10,14 +10,12 @@ "license": "MIT", "files": [ "README.md", - "LICENSE", "package.json", - "types.d.ts", + "index.d.ts", "node/index.js", "node/index_bg.wasm", "web/index.js", - "web/index_bg.wasm", - "automerge_wasm.js" + "web/index_bg.wasm" ], "types": "index.d.ts", "module": "./web/index.js", From ea2f29d681edfeebff7ec13190bd0e3a8024fb41 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 21:08:28 -0500 Subject: [PATCH 04/45] wasm to 0.0.6 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index e74cf803..6d487f51 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "files": [ "README.md", From 015e8ce465cba4c8369f5995e6b1093f77caaf64 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 15 Feb 2022 14:40:40 -0500 Subject: [PATCH 05/45] choking on bad value function --- automerge-wasm/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index b2f8e277..3ea5dff6 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -11,7 +11,9 @@ mod interop; mod sync; mod value; -use interop::{get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS}; +use interop::{ + get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, +}; use sync::SyncState; use value::{datatype, ScalarValue}; From 36b4f08d202f4e57b89e16dd57d784cf9a4a2c0c Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Feb 2022 21:08:28 -0500 Subject: [PATCH 06/45] wasm to 0.0.7 --- automerge-wasm/package.json | 6 +++--- automerge-wasm/src/lib.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 6d487f51..1a135c50 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.6", + "version": "0.0.8", "license": "MIT", "files": [ "README.md", @@ -22,8 +22,8 @@ "main": "./node/index.js", "scripts": { "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-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", + "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --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" }, diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 3ea5dff6..2c61368d 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -310,7 +310,7 @@ impl Automerge { pub fn mark( &mut self, - obj: String, + obj: JsValue, range: JsValue, name: JsValue, value: JsValue, @@ -337,7 +337,7 @@ impl Automerge { Ok(()) } - pub fn spans(&mut self, obj: String) -> Result { + 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)?; @@ -370,7 +370,7 @@ impl Automerge { Ok(result.into()) } - pub fn raw_spans(&mut self, obj: String) -> Result { + 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(); From 4f9b95b5b83b702c66c7f04dd81913ac6cc3ecb5 Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Wed, 23 Feb 2022 10:11:43 -0800 Subject: [PATCH 07/45] add test for merge behaviour of marks --- automerge-wasm/test/marks.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index ab08636f..9597e9f4 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -55,6 +55,23 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); }) + it('should handle sticky marks at the beginning of a string (..)', () => { + let doc = create() + let list = doc.set("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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 sticky marks with deleted ends (..)', () => { let doc = create() let list = doc.set("_root", "list", TEXT) From 5eb5714c131bb539571e838247ca4353c889172a Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Wed, 23 Feb 2022 21:30:47 -0800 Subject: [PATCH 08/45] add failing test for marks handling in 3-way merge scenario --- automerge-wasm/test/marks.ts | 74 +++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 9597e9f4..8064f953 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -20,6 +20,63 @@ describe('Automerge', () => { 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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("_root", "list", TEXT) + if (!list) throw new Error('should not be undefined') + 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("_root", "list", TEXT) @@ -55,23 +112,6 @@ describe('Automerge', () => { assert.deepStrictEqual(spans, [ 'aaa', [ [ 'bold', 'boolean', true ] ], 'AbbbA', [], 'ccc' ]); }) - it('should handle sticky marks at the beginning of a string (..)', () => { - let doc = create() - let list = doc.set("_root", "list", TEXT) - if (!list) throw new Error('should not be undefined') - 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 sticky marks with deleted ends (..)', () => { let doc = create() let list = doc.set("_root", "list", TEXT) From a37d4a6870f6b2ed50c359a67d54b116f38dc640 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 16:41:01 -0500 Subject: [PATCH 09/45] spans will now respect non-graphmem values --- automerge-wasm/package.json | 2 +- automerge-wasm/src/lib.rs | 18 ++++++++----- automerge/src/automerge.rs | 45 ++++++++++++++++++++++++++------ automerge/src/query.rs | 4 +-- automerge/src/query/raw_spans.rs | 21 ++++++++++----- automerge/src/value.rs | 14 ++++++++++ 6 files changed, 81 insertions(+), 23 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 1a135c50..30e0690a 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -25,7 +25,7 @@ "release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --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" + "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/marks.ts" }, "dependencies": {}, "devDependencies": { diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 2c61368d..7d886788 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -11,9 +11,7 @@ mod interop; mod sync; mod value; -use interop::{ - get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, -}; +use interop::{get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS}; use sync::SyncState; use value::{datatype, ScalarValue}; @@ -339,7 +337,7 @@ impl Automerge { 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 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(); @@ -354,7 +352,11 @@ impl Automerge { } let text_span = &text[last_pos..s.pos]; //.slice(last_pos, s.pos); if !text_span.is_empty() { - result.push(&text_span.into()); + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); } result.push(&marks); last_pos = s.pos; @@ -365,7 +367,11 @@ impl Automerge { } let text_span = &text[last_pos..]; if !text_span.is_empty() { - result.push(&text_span.into()); + let t: String = text_span + .iter() + .filter_map(|(v, _)| v.as_string()) + .collect(); + result.push(&t.into()); } Ok(result.into()) } diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 122fa884..b0f84d5f 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -453,6 +453,31 @@ impl Automerge { Ok(buffer) } + pub fn list(&self, obj: &ExId) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + 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: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + 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: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let mut query = self.ops.search(obj, query::Spans::new()); @@ -463,14 +488,18 @@ impl Automerge { pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); - let result = query.spans.into_iter().map(|s| SpanInfo { - id: self.id_to_exid(s.id), - time: self.history[s.change].time, - start: s.start, - end: s.end, - span_type: s.name, - value: s.value, - }).collect(); + let result = query + .spans + .into_iter() + .map(|s| SpanInfo { + id: self.id_to_exid(s.id), + time: self.history[s.change].time, + start: s.start, + end: s.end, + span_type: s.name, + value: s.value, + }) + .collect(); Ok(result) } diff --git a/automerge/src/query.rs b/automerge/src/query.rs index ff97532e..e630d080 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -16,9 +16,9 @@ mod nth; mod nth_at; mod prop; mod prop_at; +mod raw_spans; mod seek_op; mod spans; -mod raw_spans; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; @@ -31,9 +31,9 @@ pub(crate) use nth::Nth; pub(crate) use nth_at::NthAt; 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}; -pub(crate) use raw_spans::RawSpans; #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs index 77a45741..95aafc56 100644 --- a/automerge/src/query/raw_spans.rs +++ b/automerge/src/query/raw_spans.rs @@ -36,7 +36,6 @@ impl RawSpans { } impl TreeQuery for RawSpans { - fn query_element_with_metadata(&mut self, element: &Op, m: &OpSetMetadata) -> QueryResult { // find location to insert // mark or set @@ -46,14 +45,24 @@ impl TreeQuery for RawSpans { .spans .binary_search_by(|probe| m.lamport_cmp(probe.id, element.id)) .unwrap_err(); - self.spans.insert(pos, RawSpan { id: element.id, change: element.change, start: self.seen, end: 0, name: md.name.clone(), value: md.value.clone() }); + self.spans.insert( + pos, + RawSpan { + id: element.id, + change: element.change, + 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 s.id == element.id.prev() { + s.end = self.seen; + break; + } } } } diff --git a/automerge/src/value.rs b/automerge/src/value.rs index ac26033c..16364f34 100644 --- a/automerge/src/value.rs +++ b/automerge/src/value.rs @@ -18,6 +18,13 @@ 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) } @@ -394,6 +401,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()) } From a84fa6455428e725ab03be40299f274552e8da2e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 23 Feb 2022 19:43:13 -0500 Subject: [PATCH 10/45] change MAP,LIST,TEXT to be {},[],'' - allow recursion --- automerge-js/src/proxies.js | 25 ++++++----- automerge-wasm/index.d.ts | 17 +++----- automerge-wasm/src/interop.rs | 72 +++++++++++++++++++++++-------- automerge-wasm/src/lib.rs | 80 +++++++++++++++++++++-------------- automerge-wasm/test/test.ts | 54 ++++++++++++++--------- edit-trace/automerge-wasm.js | 4 +- 6 files changed, 158 insertions(+), 94 deletions(-) diff --git a/automerge-js/src/proxies.js b/automerge-js/src/proxies.js index 878ae101..ed3a4b97 100644 --- a/automerge-js/src/proxies.js +++ b/automerge-js/src/proxies.js @@ -4,7 +4,6 @@ const { Int, Uint, Float64 } = require("./numbers"); const { Counter, getWriteableCounter } = require("./counter"); const { Text } = require("./text"); const { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY } = require("./constants") -const { MAP, LIST, TABLE, TEXT } = require("automerge-wasm") function parseListIndex(key) { if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10) @@ -135,21 +134,21 @@ const MapHandler = { } switch (datatype) { case "list": - const list = context.set(objectId, key, LIST) + const list = context.set(objectId, key, []) const proxyList = listProxy(context, list, [ ... path, key ], readonly ); for (let i = 0; i < value.length; i++) { proxyList[i] = value[i] } break; case "text": - const text = context.set(objectId, key, TEXT) + const text = context.set(objectId, key, "", "text") const proxyText = textProxy(context, text, [ ... path, key ], readonly ); for (let i = 0; i < value.length; i++) { proxyText[i] = value.get(i) } break; case "map": - const map = context.set(objectId, key, MAP) + const map = context.set(objectId, key, {}) const proxyMap = mapProxy(context, map, [ ... path, key ], readonly ); for (const key in value) { proxyMap[key] = value[key] @@ -252,9 +251,9 @@ const ListHandler = { case "list": let list if (index >= context.length(objectId)) { - list = context.insert(objectId, index, LIST) + list = context.insert(objectId, index, []) } else { - list = context.set(objectId, index, LIST) + list = context.set(objectId, index, []) } const proxyList = listProxy(context, list, [ ... path, index ], readonly); proxyList.splice(0,0,...value) @@ -262,9 +261,9 @@ const ListHandler = { case "text": let text if (index >= context.length(objectId)) { - text = context.insert(objectId, index, TEXT) + text = context.insert(objectId, index, "", "text") } else { - text = context.set(objectId, index, TEXT) + text = context.set(objectId, index, "", "text") } const proxyText = textProxy(context, text, [ ... path, index ], readonly); proxyText.splice(0,0,...value) @@ -272,9 +271,9 @@ const ListHandler = { case "map": let map if (index >= context.length(objectId)) { - map = context.insert(objectId, index, MAP) + map = context.insert(objectId, index, {}) } else { - map = context.set(objectId, index, MAP) + map = context.set(objectId, index, {}) } const proxyMap = mapProxy(context, map, [ ... path, index ], readonly); for (const key in value) { @@ -479,17 +478,17 @@ function listMethods(target) { for (let [value,datatype] of values) { switch (datatype) { case "list": - const list = context.insert(objectId, index, LIST) + const list = context.insert(objectId, index, []) const proxyList = listProxy(context, list, [ ... path, index ], readonly); proxyList.splice(0,0,...value) break; case "text": - const text = context.insert(objectId, index, TEXT) + const text = context.insert(objectId, index, "", "text") const proxyText = textProxy(context, text, [ ... path, index ], readonly); proxyText.splice(0,0,...value) break; case "map": - const map = context.insert(objectId, index, MAP) + const map = context.insert(objectId, index, {}) const proxyMap = mapProxy(context, map, [ ... path, index ], readonly); for (const key in value) { proxyMap[key] = value[key] diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 8a7e9408..7b97583e 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -6,8 +6,7 @@ export type SyncMessage = Uint8Array; export type Prop = string | number; export type Hash = string; export type Heads = Hash[]; -export type ObjectType = string; // opaque ?? -export type Value = string | number | boolean | null | Date | Uint8Array | ObjectType; +export type Value = string | number | boolean | null | Date | Uint8Array | Array | Object; export type FullValue = ["str", string] | ["int", number] | @@ -23,11 +22,6 @@ export type FullValue = ["text", ObjID] | ["table", ObjID] -export const LIST : ObjectType; -export const MAP : ObjectType; -export const TABLE : ObjectType; -export const TEXT : ObjectType; - export enum ObjTypeName { list = "list", map = "map", @@ -44,7 +38,10 @@ export type Datatype = "null" | "timestamp" | "counter" | - "bytes"; + "bytes" | + "map" | + "text" | + "list"; export type DecodedSyncMessage = { heads: Heads, @@ -86,10 +83,10 @@ export function decodeSyncState(data: Uint8Array): SyncState; export class Automerge { // change state set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID | undefined; - make(obj: ObjID, prop: Prop, value: ObjectType): ObjID; + make(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID; insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): ObjID | undefined; push(obj: ObjID, value: Value, datatype?: Datatype): ObjID | undefined; - splice(obj: ObjID, start: number, delete_count: number, text?: string | Array): ObjID[] | undefined; + splice(obj: ObjID, start: number, delete_count: number, text?: string | Array): ObjID[] | undefined; inc(obj: ObjID, prop: Prop, value: number): void; del(obj: ObjID, prop: Prop): void; diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index fc4c39f9..17a46251 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -3,6 +3,7 @@ use automerge::{Change, ChangeHash, Prop}; use js_sys::{Array, Object, Reflect, Uint8Array}; use std::collections::HashSet; use std::fmt::Display; +use unicode_segmentation::UnicodeSegmentation; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -257,23 +258,60 @@ pub(crate) fn to_prop(p: JsValue) -> Result { } } -pub(crate) fn to_objtype(a: &JsValue) -> Option { - if !a.is_function() { - return None; - } - let f: js_sys::Function = a.clone().try_into().unwrap(); - let f = f.to_string(); - if f.starts_with("class MAP", 0) { - Some(am::ObjType::Map) - } else if f.starts_with("class LIST", 0) { - Some(am::ObjType::List) - } else if f.starts_with("class TEXT", 0) { - Some(am::ObjType::Text) - } else if f.starts_with("class TABLE", 0) { - Some(am::ObjType::Table) - } else { - am::log!("to_objtype(function) -> {}", f); - None +pub(crate) fn to_objtype( + value: &JsValue, + datatype: &Option, +) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> { + match datatype.as_deref() { + Some("map") => { + let map = value.clone().dyn_into::().ok()?; + // FIXME unwrap + let map = js_sys::Object::keys(&map) + .iter() + .zip(js_sys::Object::values(&map).iter()) + .map(|(key, val)| (key.as_string().unwrap().into(), val)) + .collect(); + Some((am::ObjType::Map, map)) + } + Some("list") => { + let list = value.clone().dyn_into::().ok()?; + let list = list + .iter() + .enumerate() + .map(|(i, e)| (i.into(), e)) + .collect(); + Some((am::ObjType::List, list)) + } + Some("text") => { + let text = value.as_string()?; + let text = text + .graphemes(true) + .enumerate() + .map(|(i, ch)| (i.into(), ch.into())) + .collect(); + Some((am::ObjType::Text, text)) + } + Some(_) => None, + None => { + if let Ok(list) = value.clone().dyn_into::() { + let list = list + .iter() + .enumerate() + .map(|(i, e)| (i.into(), e)) + .collect(); + Some((am::ObjType::List, list)) + } else if let Ok(map) = value.clone().dyn_into::() { + // FIXME unwrap + let map = js_sys::Object::keys(&map) + .iter() + .zip(js_sys::Object::values(&map).iter()) + .map(|(key, val)| (key.as_string().unwrap().into(), val)) + .collect(); + Some((am::ObjType::Map, map)) + } else { + None + } + } } } diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 7d886788..4fc2c384 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -132,15 +132,11 @@ impl Automerge { } 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)?; - vals.push(value); - } else { - let value = self.import_value(i, JsValue::null())?; - vals.push(value); + let (value, subvals) = self.import_value(&i, None)?; + if !subvals.is_empty() { + return Err(to_js_err("splice must be shallow")); } + vals.push(value); } } let result = self.0.splice(&obj, start, delete_count, vals)?; @@ -163,9 +159,10 @@ impl Automerge { datatype: JsValue, ) -> Result, JsValue> { let obj = self.import(obj)?; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let index = self.0.length(&obj); let opid = self.0.insert(&obj, index, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string())) } @@ -178,8 +175,9 @@ impl Automerge { ) -> Result, JsValue> { let obj = self.import(obj)?; let index = index as f64; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let opid = self.0.insert(&obj, index as usize, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string())) } @@ -192,17 +190,44 @@ impl Automerge { ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let value = self.import_value(value, datatype)?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; let opid = self.0.set(&obj, prop, value)?; + self.subset(&opid, subvals)?; Ok(opid.map(|id| id.to_string()).into()) } - pub fn make(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result { + fn subset( + &mut self, + obj: &Option, + vals: Vec<(am::Prop, JsValue)>, + ) -> Result<(), JsValue> { + if let Some(id) = obj { + for (p, v) in vals { + let (value, subvals) = self.import_value(&v, None)?; + //let opid = self.0.set(id, p, value)?; + let opid = match p { + Prop::Map(s) => self.0.set(id, s, value)?, + Prop::Seq(i) => self.0.insert(id, i, value)?, + }; + self.subset(&opid, subvals)?; + } + } + Ok(()) + } + + pub fn make( + &mut self, + obj: JsValue, + prop: JsValue, + value: JsValue, + datatype: JsValue, + ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let value = self.import_value(value, JsValue::null())?; + let (value, subvals) = self.import_value(&value, datatype.as_string())?; if value.is_object() { let opid = self.0.set(&obj, prop, value)?; + self.subset(&opid, subvals)?; Ok(opid.unwrap().to_string()) } else { Err(to_js_err("invalid object type")) @@ -561,15 +586,18 @@ impl Automerge { } } - fn import_value(&mut self, value: JsValue, datatype: JsValue) -> Result { - let d = datatype.as_string(); - match self.import_scalar(&value, &d) { - Some(val) => Ok(val.into()), + fn import_value( + &mut self, + value: &JsValue, + datatype: Option, + ) -> Result<(Value, Vec<(Prop, JsValue)>), JsValue> { + match self.import_scalar(value, &datatype) { + Some(val) => Ok((val.into(), vec![])), None => { - if let Some(o) = to_objtype(&value) { - Ok(o.into()) + if let Some((o, subvals)) = to_objtype(value, &datatype) { + Ok((o.into(), subvals)) } else { - web_sys::console::log_3(&"Invalid value".into(), &value, &datatype); + web_sys::console::log_2(&"Invalid value".into(), value); Err(to_js_err("invalid value")) } } @@ -672,15 +700,3 @@ pub fn encode_sync_state(state: SyncState) -> Result { 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/test/test.ts b/automerge-wasm/test/test.ts index 51e50789..93547b8e 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -3,7 +3,7 @@ import { describe, it } from 'mocha'; import assert from 'assert' //@ts-ignore import { BloomFilter } from './helpers/sync' -import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' +import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '..' import { DecodedSyncMessage } from '..'; import { Hash } from '..'; @@ -64,7 +64,7 @@ describe('Automerge', () => { doc.set(root, "bool", true) doc.set(root, "time1", 1000, "timestamp") doc.set(root, "time2", new Date(1001)) - doc.set(root, "list", LIST); + doc.set(root, "list", []); doc.set(root, "null", null) result = doc.value(root,"hello") @@ -124,7 +124,7 @@ describe('Automerge', () => { let root = "_root" let result - let submap = doc.set(root, "submap", MAP) + let submap = doc.set(root, "submap", {}) if (!submap) throw new Error('should be not null') doc.set(submap, "number", 6, "uint") assert.strictEqual(doc.pendingOps(),2) @@ -141,7 +141,7 @@ describe('Automerge', () => { let doc = create() let root = "_root" - let submap = doc.set(root, "numbers", LIST) + let submap = doc.set(root, "numbers", []) if (!submap) throw new Error('should be not null') doc.insert(submap, 0, "a"); doc.insert(submap, 1, "b"); @@ -165,7 +165,7 @@ describe('Automerge', () => { let doc = create() let root = "_root" - let submap = doc.set(root, "letters", LIST) + let submap = doc.set(root, "letters", []) if (!submap) throw new Error('should be not null') doc.insert(submap, 0, "a"); doc.insert(submap, 0, "b"); @@ -230,11 +230,11 @@ describe('Automerge', () => { let doc = create() let root = "_root"; - let text = doc.set(root, "text", TEXT); + let text = doc.set(root, "text", "", "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, [["str","!"],["str","?"]]) + doc.splice(text, 11, 0, ["!","?"]) assert.deepEqual(doc.value(text, 0),["str","h"]) assert.deepEqual(doc.value(text, 1),["str","e"]) assert.deepEqual(doc.value(text, 9),["str","l"]) @@ -282,7 +282,7 @@ describe('Automerge', () => { it('should be able to splice text', () => { let doc = create() - let text = doc.set("_root", "text", TEXT); + let text = doc.set("_root", "text", "", "text"); if (!text) throw new Error('should not be undefined') doc.splice(text, 0, 0, "hello world"); let heads1 = doc.commit(); @@ -331,7 +331,7 @@ describe('Automerge', () => { it('local inc increments all visible counters in a sequence', () => { let doc1 = create("aaaa") - let seq = doc1.set("_root", "seq", LIST) + let seq = doc1.set("_root", "seq", []) if (!seq) throw new Error('Should not be undefined') doc1.insert(seq, 0, "hello") let doc2 = loadDoc(doc1.save(), "bbbb"); @@ -363,18 +363,32 @@ describe('Automerge', () => { doc4.free() }) + it('recursive sets are possible', () => { + let doc = create("aaaa") + let l1 = doc.make("_root","list",[{ foo: "bar"}, [1,2,3]]) + let l2 = doc.insert(l1, 0, { zip: ["a", "b"] }) + let l3 = doc.set("_root","info1","hello world","text") + let l4 = doc.set("_root","info2","hello world") + assert.deepEqual(doc.toJS(), { + "list": [ { zip: ["a", "b"] }, { foo: "bar"}, [ 1,2,3]], + "info1": "hello world".split(""), + "info2": "hello world" + }) + doc.free() + }) + it('only returns an object id when objects are created', () => { let doc = create("aaaa") let r1 = doc.set("_root","foo","bar") - let r2 = doc.set("_root","list",LIST) + let r2 = doc.set("_root","list",[]) let r3 = doc.set("_root","counter",10, "counter") let r4 = doc.inc("_root","counter",1) let r5 = doc.del("_root","counter") if (!r2) throw new Error('should not be undefined') let r6 = doc.insert(r2,0,10); - let r7 = doc.insert(r2,0,MAP); + let r7 = doc.insert(r2,0,{}); let r8 = doc.splice(r2,1,0,["a","b","c"]); - let r9 = doc.splice(r2,1,0,["a",LIST,MAP,"d"]); + let r9 = doc.splice(r2,1,0,["a",[],{},"d"]); assert.deepEqual(r1,null); assert.deepEqual(r2,"2@aaaa"); assert.deepEqual(r3,null); @@ -389,11 +403,11 @@ describe('Automerge', () => { it('objects without properties are preserved', () => { let doc1 = create("aaaa") - let a = doc1.set("_root","a",MAP); + let a = doc1.set("_root","a",{}); if (!a) throw new Error('should not be undefined') - let b = doc1.set("_root","b",MAP); + let b = doc1.set("_root","b",{}); if (!b) throw new Error('should not be undefined') - let c = doc1.set("_root","c",MAP); + let c = doc1.set("_root","c",{}); if (!c) throw new Error('should not be undefined') let d = doc1.set(c,"d","dd"); let saved = doc1.save(); @@ -411,7 +425,7 @@ describe('Automerge', () => { it('should handle merging text conflicts then saving & loading', () => { let A = create("aabbcc") - let At = A.make('_root', 'text', TEXT) + let At = A.make('_root', 'text', "", "text") A.splice(At, 0, 0, 'hello') let B = A.fork() @@ -462,7 +476,7 @@ describe('Automerge', () => { let s1 = initSyncState(), s2 = initSyncState() // make two nodes with the same changes - let list = n1.set("_root","n", LIST) + let list = n1.set("_root","n", []) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -486,7 +500,7 @@ describe('Automerge', () => { let n1 = create(), n2 = create() // make changes for n1 that n2 should request - let list = n1.set("_root","n",LIST) + let list = n1.set("_root","n",[]) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -503,7 +517,7 @@ describe('Automerge', () => { let n1 = create(), n2 = create() // make changes for n1 that n2 should request - let list = n1.set("_root","n",LIST) + let list = n1.set("_root","n",[]) if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { @@ -660,7 +674,7 @@ describe('Automerge', () => { let n1 = create('01234567'), n2 = create('89abcdef') let s1 = initSyncState(), s2 = initSyncState(), message = null - let items = n1.set("_root", "items", LIST) + let items = n1.set("_root", "items", []) if (!items) throw new Error('undefined') n1.commit("",0) diff --git a/edit-trace/automerge-wasm.js b/edit-trace/automerge-wasm.js index 02130686..3680efc0 100644 --- a/edit-trace/automerge-wasm.js +++ b/edit-trace/automerge-wasm.js @@ -10,8 +10,8 @@ const Automerge = require('../automerge-wasm') const start = new Date() -let doc = Automerge.init(); -let text = doc.set("_root", "text", Automerge.TEXT) +let doc = Automerge.create(); +let text = doc.set("_root", "text", "", "text") for (let i = 0; i < edits.length; i++) { let edit = edits[i] From e37395f975dc07b6a1a5e1e0ea2b544ca1173b7f Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 00:22:56 -0500 Subject: [PATCH 11/45] make() defaults to text --- automerge-wasm/src/interop.rs | 7 +++++++ automerge-wasm/src/lib.rs | 11 ++--------- automerge-wasm/test/test.ts | 8 +++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index 17a46251..61cc81e4 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -308,6 +308,13 @@ pub(crate) fn to_objtype( .map(|(key, val)| (key.as_string().unwrap().into(), val)) .collect(); Some((am::ObjType::Map, map)) + } else if let Some(text) = value.as_string() { + let text = text + .graphemes(true) + .enumerate() + .map(|(i, ch)| (i.into(), ch.into())) + .collect(); + Some((am::ObjType::Text, text)) } else { None } diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 4fc2c384..296a503b 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -215,17 +215,10 @@ impl Automerge { Ok(()) } - pub fn make( - &mut self, - obj: JsValue, - prop: JsValue, - value: JsValue, - datatype: JsValue, - ) -> Result { + pub fn make(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let (value, subvals) = self.import_value(&value, datatype.as_string())?; - if value.is_object() { + if let Some((value, subvals)) = to_objtype(&value, &None) { let opid = self.0.set(&obj, prop, value)?; self.subset(&opid, subvals)?; Ok(opid.unwrap().to_string()) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 93547b8e..9b721f90 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -367,12 +367,14 @@ describe('Automerge', () => { let doc = create("aaaa") let l1 = doc.make("_root","list",[{ foo: "bar"}, [1,2,3]]) let l2 = doc.insert(l1, 0, { zip: ["a", "b"] }) - let l3 = doc.set("_root","info1","hello world","text") - let l4 = doc.set("_root","info2","hello world") + let l3 = doc.make("_root","info1","hello world") // 'text' + let l4 = doc.set("_root","info2","hello world") // 'str' + let l5 = doc.set("_root","info3","hello world", "text") assert.deepEqual(doc.toJS(), { "list": [ { zip: ["a", "b"] }, { foo: "bar"}, [ 1,2,3]], "info1": "hello world".split(""), - "info2": "hello world" + "info2": "hello world", + "info3": "hello world".split("") }) doc.free() }) From 872efc5756a524f11cfc1dd37a76287f8080b12d Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 00:23:32 -0500 Subject: [PATCH 12/45] v10 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 30e0690a..bef3f24c 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.8", + "version": "0.0.10", "license": "MIT", "files": [ "README.md", From 3c3f411329785b013c59d4e2851252a29a1bfbb9 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 18:43:44 -0500 Subject: [PATCH 13/45] update to new autotransaction api --- automerge-wasm/package.json | 4 +- automerge-wasm/test/marks.ts | 18 +-- automerge-wasm/test/test.ts | 1 + automerge/src/autocommit.rs | 49 ++++++++- automerge/src/automerge.rs | 103 +++++------------- automerge/src/query.rs | 13 +++ automerge/src/transaction/inner.rs | 36 +++++- .../src/transaction/manual_transaction.rs | 45 +++++++- automerge/src/transaction/transactable.rs | 32 +++++- automerge/tests/test.rs | 96 ++++++++-------- 10 files changed, 256 insertions(+), 141 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index bef3f24c..88912336 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.10", + "version": "0.0.11", "license": "MIT", "files": [ "README.md", @@ -25,7 +25,7 @@ "release-w": "rimraf ./web && wasm-pack build --target web --dev --out-name index -d web && cp index.d.ts web", "release-n": "rimraf ./node && wasm-pack build --target nodejs --dev --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/marks.ts" + "test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts" }, "dependencies": {}, "devDependencies": { diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 8064f953..76702caf 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -2,13 +2,13 @@ import { describe, it } from 'mocha'; //@ts-ignore import assert from 'assert' //@ts-ignore -import { create, loadDoc, Automerge, TEXT, encodeChange, decodeChange } from '..' +import { create, loadDoc, Automerge, encodeChange, decodeChange } from '..' describe('Automerge', () => { describe('marks', () => { it('should handle marks [..]', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) @@ -22,7 +22,7 @@ describe('Automerge', () => { it('should handle marks [..] at the beginning of a string', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -39,7 +39,7 @@ describe('Automerge', () => { it('should handle marks [..] with splice', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -56,7 +56,7 @@ describe('Automerge', () => { it('should handle marks across multiple forks', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[0..3]", "bold", true) @@ -79,7 +79,7 @@ describe('Automerge', () => { it('should handle marks with deleted ends [..]', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") @@ -100,7 +100,7 @@ describe('Automerge', () => { it('should handle sticky marks (..)', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) @@ -114,7 +114,7 @@ describe('Automerge', () => { it('should handle sticky marks with deleted ends (..)', () => { let doc = create() - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "(3..6)", "bold" , true) @@ -143,7 +143,7 @@ describe('Automerge', () => { it('should handle overlapping marks', () => { let doc : Automerge = create("aabbcc") - let list = doc.set("_root", "list", TEXT) + let list = doc.make("_root", "list", "") if (!list) throw new Error('should not be undefined') doc.splice(list, 0, 0, "the quick fox jumps over the lazy dog") doc.mark(list, "[0..37]", "bold" , true) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 8375e625..9d7a8960 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -4,6 +4,7 @@ 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 '..' function sync(a: Automerge, b: Automerge, aSyncState = initSyncState(), bSyncState = initSyncState()) { const MAX_ITER = 10 diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index f54fea03..a5673a57 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -2,8 +2,8 @@ use crate::exid::ExId; use crate::transaction::{CommitOptions, Transactable}; use crate::types::Patch; use crate::{ - change::export_change, transaction::TransactionInner, ActorId, Automerge, AutomergeError, - Change, ChangeHash, Prop, Value, + change::export_change, query, transaction::TransactionInner, ActorId, Automerge, + AutomergeError, Change, ChangeHash, Prop, ScalarValue, Value, }; use crate::{SyncMessage, SyncState}; @@ -347,6 +347,31 @@ impl Transactable for AutoCommit { tx.insert(&mut self.doc, obj, index, value) } + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + 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 inc>( &mut self, obj: &ExId, @@ -386,6 +411,26 @@ impl Transactable for AutoCommit { self.doc.text_at(obj, heads) } + fn list(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + // 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 4db083e8..5a0ef99d 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -13,7 +13,6 @@ use crate::types::{ }; use crate::{legacy, query, types, ObjType}; use crate::{AutomergeError, Change, Prop}; -use serde::Serialize; /// An automerge document. #[derive(Debug, Clone)] @@ -316,13 +315,13 @@ impl Automerge { Ok(query.spans) } - pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); let result = query .spans .into_iter() - .map(|s| SpanInfo { + .map(|s| query::SpanInfo { id: self.id_to_exid(s.id), time: self.history[s.change].time, start: s.start, @@ -334,68 +333,37 @@ impl Automerge { Ok(result) } - #[allow(clippy::too_many_arguments)] - pub fn mark( - &mut self, - obj: &ExId, - start: usize, - expand_start: bool, - end: usize, - expand_end: bool, - mark: &str, - value: ScalarValue, - ) -> Result<(), AutomergeError> { - let obj = self.exid_to_obj(obj)?; + /* + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = self.exid_to_obj(obj)?; - self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; - self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; + self.do_insert(obj, start, OpType::mark(mark.into(), expand_start, value))?; + self.do_insert(obj, end, OpType::MarkEnd(expand_end))?; - /* - let (a, b) = query.ops()?; - let (pos, key) = a; - let id = self.next_id(); - let op = Op { - change: self.history.len(), - id, - action: OpType::Mark(MarkData { name: mark.into(), expand: expand_start, value}), - obj, - key, - succ: Default::default(), - pred: Default::default(), - insert: true, - }; - self.ops.insert(pos, op.clone()); - self.tx().operations.push(op); + Ok(()) + } - let (pos, key) = b; - let id = self.next_id(); - let op = Op { - change: self.history.len(), - id, - action: OpType::Unmark(expand_end), - obj, - key, - succ: Default::default(), - pred: Default::default(), - insert: true, - }; - self.ops.insert(pos, op.clone()); - self.tx().operations.push(op); - */ - - Ok(()) - } - - pub fn unmark( - &self, - _obj: &ExId, - _start: usize, - _end: usize, - _inclusive: bool, - _mark: &str, - ) -> Result { - unimplemented!() - } + pub fn unmark( + &self, + _obj: &ExId, + _start: usize, + _end: usize, + _inclusive: bool, + _mark: &str, + ) -> Result { + unimplemented!() + } + */ // TODO - I need to return these OpId's here **only** to get // the legacy conflicts format of { [opid]: value } @@ -966,17 +934,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 super::*; diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 1b97ec51..19ea30ce 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -1,6 +1,8 @@ +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; @@ -37,6 +39,17 @@ 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 time: i64, + pub start: usize, + pub end: usize, + #[serde(rename = "type")] + pub span_type: String, + pub value: ScalarValue, +} + #[derive(Debug, Clone, PartialEq)] pub(crate) struct CounterData { pos: usize, diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index aaa26a99..d476e4e9 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -1,7 +1,7 @@ use crate::exid::ExId; use crate::query::{self, OpIdSearch}; use crate::types::{Key, ObjId, OpId}; -use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, Value}; +use crate::{change::export_change, types::Op, Automerge, ChangeHash, Prop, ScalarValue, Value}; use crate::{AutomergeError, OpType}; #[derive(Debug, Clone)] @@ -116,6 +116,7 @@ impl TransactionInner { value: V, ) -> Result, AutomergeError> { let obj = doc.exid_to_obj(obj)?; + let value = value.into(); if let Some(id) = self.do_insert(doc, obj, index, value)? { Ok(Some(doc.id_to_exid(id))) } else { @@ -123,20 +124,45 @@ impl TransactionInner { } } - fn do_insert>( + #[allow(clippy::too_many_arguments)] + pub fn mark( + &mut self, + doc: &mut Automerge, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError> { + let obj = doc.exid_to_obj(obj)?; + + self.do_insert( + doc, + obj, + start, + OpType::mark(mark.into(), expand_start, value), + )?; + self.do_insert(doc, obj, end, OpType::MarkEnd(expand_end))?; + + Ok(()) + } + + fn do_insert>( &mut self, doc: &mut Automerge, obj: ObjId, index: usize, - value: V, + action: V, ) -> Result, AutomergeError> { let id = self.next_id(); let query = doc.ops.search(obj, query::InsertNth::new(index)); let key = query.key()?; - let value = value.into(); - let action = value.into(); + //let value = value.into(); + let action = action.into(); 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 d52b9219..cd341702 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::AutomergeError; -use crate::{Automerge, ChangeHash, Prop, Value}; +use crate::{query, Automerge, ChangeHash, Prop, ScalarValue, Value}; use super::{CommitOptions, Transactable, TransactionInner}; @@ -106,6 +106,29 @@ impl<'a> Transactable for Transaction<'a> { .insert(self.doc, obj, index, value) } + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + 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 inc>( &mut self, obj: &ExId, @@ -158,6 +181,26 @@ impl<'a> Transactable for Transaction<'a> { self.doc.text_at(obj, heads) } + fn list(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.list(obj) + } + + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError> { + self.doc.list_at(obj, heads) + } + + fn spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.spans(obj) + } + + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { + self.doc.raw_spans(obj) + } + fn value>( &self, obj: &ExId, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index dbce1d14..5e4b21f5 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -1,5 +1,6 @@ use crate::exid::ExId; -use crate::{AutomergeError, ChangeHash, Prop, Value}; +use crate::query; +use crate::{AutomergeError, ChangeHash, Prop, ScalarValue, Value}; use unicode_segmentation::UnicodeSegmentation; /// A way of mutating a document within a single change. @@ -35,6 +36,19 @@ pub trait Transactable { value: V, ) -> Result, AutomergeError>; + /// Set a mark within a range on a list + #[allow(clippy::too_many_arguments)] + fn mark( + &mut self, + obj: &ExId, + start: usize, + expand_start: bool, + end: usize, + expand_end: bool, + mark: &str, + value: ScalarValue, + ) -> Result<(), AutomergeError>; + /// Increment the counter at the prop in the object by `value`. fn inc>(&mut self, obj: &ExId, prop: P, value: i64) -> Result<(), AutomergeError>; @@ -85,6 +99,22 @@ pub trait Transactable { /// Get the string that this text object represents at a point in history. fn text_at(&self, obj: &ExId, heads: &[ChangeHash]) -> Result; + /// Get the string that this text object represents. + fn list(&self, obj: &ExId) -> Result, AutomergeError>; + + /// Get the string that this text object represents at a point in history. + fn list_at( + &self, + obj: &ExId, + heads: &[ChangeHash], + ) -> Result, AutomergeError>; + + /// test spans api for mark/span experiment + fn spans(&self, obj: &ExId) -> Result, AutomergeError>; + + /// test raw_spans api for mark/span experiment + fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError>; + /// Get the value at this prop in the object. fn value>( &self, diff --git a/automerge/tests/test.rs b/automerge/tests/test.rs index 7d272a88..34a9777b 100644 --- a/automerge/tests/test.rs +++ b/automerge/tests/test.rs @@ -55,10 +55,10 @@ fn repeated_map_assignment_which_resolves_conflict_not_ignored() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "field", 123).unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&automerge::ROOT, "field", 456).unwrap(); doc1.set(&automerge::ROOT, "field", 789).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_eq!(doc1.values(&automerge::ROOT, "field").unwrap().len(), 2); doc1.set(&automerge::ROOT, "field", 123).unwrap(); @@ -79,9 +79,9 @@ fn repeated_list_assignment_which_resolves_conflict_not_ignored() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, 123).unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&list_id, 0, 456).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc1.set(&list_id, 0, 789).unwrap(); assert_doc!( @@ -124,7 +124,7 @@ fn merge_concurrent_map_prop_updates() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "foo", "bar").unwrap(); doc2.set(&automerge::ROOT, "hello", "world").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_eq!( doc1.value(&automerge::ROOT, "foo").unwrap().unwrap().0, "bar".into() @@ -136,7 +136,7 @@ fn merge_concurrent_map_prop_updates() { "hello" => { "world" }, } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -153,10 +153,10 @@ fn add_concurrent_increments_of_same_property() { let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "counter", mk_counter(0)) .unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.inc(&automerge::ROOT, "counter", 1).unwrap(); doc2.inc(&automerge::ROOT, "counter", 2).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), map! { @@ -182,7 +182,7 @@ fn add_increments_only_to_preceeded_values() { doc2.inc(&automerge::ROOT, "counter", 3).unwrap(); // The two values should be conflicting rather than added - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -202,7 +202,7 @@ fn concurrent_updates_of_same_field() { doc1.set(&automerge::ROOT, "field", "one").unwrap(); doc2.set(&automerge::ROOT, "field", "two").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -224,11 +224,11 @@ fn concurrent_updates_of_same_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "finch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&list_id, 0, "greenfinch").unwrap(); doc2.set(&list_id, 0, "goldfinch").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -253,8 +253,8 @@ fn assignment_conflicts_of_different_types() { .unwrap(); doc3.set(&automerge::ROOT, "field", automerge::Value::map()) .unwrap(); - doc1.merge(&mut doc2); - doc1.merge(&mut doc3); + doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc3).unwrap(); assert_doc!( doc1.document(), @@ -278,7 +278,7 @@ fn changes_within_conflicting_map_field() { .unwrap() .unwrap(); doc2.set(&map_id, "innerKey", 42).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -305,7 +305,7 @@ fn changes_within_conflicting_list_element() { .unwrap() .unwrap(); doc1.insert(&list_id, 0, "hello").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); let map_in_doc1 = doc1 .set(&list_id, 0, automerge::Value::map()) @@ -318,11 +318,11 @@ fn changes_within_conflicting_list_element() { .set(&list_id, 0, automerge::Value::map()) .unwrap() .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc2.set(&map_in_doc2, "map2", true).unwrap(); doc2.set(&map_in_doc2, "key", 2).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -362,7 +362,7 @@ fn concurrently_assigned_nested_maps_should_not_merge() { .unwrap(); doc2.set(&doc2_map_id, "logo_url", "logo.png").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -393,11 +393,11 @@ fn concurrent_insertions_at_different_list_positions() { doc1.insert(&list_id, 0, "one").unwrap(); doc1.insert(&list_id, 1, "three").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice(&list_id, 1, 0, vec!["two".into()]).unwrap(); doc2.insert(&list_id, 2, "four").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -427,10 +427,10 @@ fn concurrent_insertions_at_same_list_position() { .unwrap(); doc1.insert(&list_id, 0, "parakeet").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.insert(&list_id, 1, "starling").unwrap(); doc2.insert(&list_id, 1, "chaffinch").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -457,11 +457,11 @@ fn concurrent_assignment_and_deletion_of_a_map_entry() { let mut doc1 = new_doc(); let mut doc2 = new_doc(); doc1.set(&automerge::ROOT, "bestBird", "robin").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&automerge::ROOT, "bestBird").unwrap(); doc2.set(&automerge::ROOT, "bestBird", "magpie").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -484,7 +484,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { doc1.insert(&list_id, 0, "blackbird").unwrap(); doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&list_id, 1, "starling").unwrap(); doc2.del(&list_id, 1).unwrap(); @@ -509,7 +509,7 @@ fn concurrent_assignment_and_deletion_of_list_entry() { } ); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -536,14 +536,14 @@ fn insertion_after_a_deleted_list_element() { doc1.insert(&list_id, 1, "thrush").unwrap(); doc1.insert(&list_id, 2, "goldfinch").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice(&list_id, 1, 2, Vec::new()).unwrap(); doc2.splice(&list_id, 2, 0, vec!["starling".into()]) .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -555,7 +555,7 @@ fn insertion_after_a_deleted_list_element() { } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -580,13 +580,13 @@ fn concurrent_deletion_of_same_list_element() { doc1.insert(&list_id, 1, "buzzard").unwrap(); doc1.insert(&list_id, 2, "cormorant").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&list_id, 1).unwrap(); doc2.del(&list_id, 1).unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -598,7 +598,7 @@ fn concurrent_deletion_of_same_list_element() { } ); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); assert_doc!( doc2.document(), map! { @@ -632,12 +632,12 @@ fn concurrent_updates_at_different_levels() { .unwrap(); doc1.insert(&mammals, 0, "badger").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.set(&birds, "brown", "sparrow").unwrap(); doc2.del(&animals, "birds").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_obj!( doc1.document(), @@ -677,13 +677,13 @@ fn concurrent_updates_of_concurrently_deleted_objects() { .unwrap(); doc1.set(&blackbird, "feathers", "black").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.del(&birds, "blackbird").unwrap(); doc2.set(&blackbird, "beak", "orange").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -705,7 +705,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { .set(&automerge::ROOT, "wisdom", automerge::Value::list()) .unwrap() .unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc1.splice( &wisdom, @@ -735,7 +735,7 @@ fn does_not_interleave_sequence_insertions_at_same_position() { ) .unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); assert_doc!( doc1.document(), @@ -768,7 +768,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_greater_actor_id( .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -794,7 +794,7 @@ fn mutliple_insertions_at_same_list_position_with_insertion_by_lesser_actor_id() .unwrap() .unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -818,11 +818,11 @@ fn insertion_consistent_with_causality() { .unwrap() .unwrap(); doc1.insert(&list, 0, "four").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "three").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); doc1.insert(&list, 0, "two").unwrap(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.insert(&list, 0, "one").unwrap(); assert_doc!( @@ -862,11 +862,11 @@ fn save_restore_complex() { doc1.set(&first_todo, "done", false).unwrap(); let mut doc2 = new_doc(); - doc2.merge(&mut doc1); + doc2.merge(&mut doc1).unwrap(); doc2.set(&first_todo, "title", "weed plants").unwrap(); doc1.set(&first_todo, "title", "kill plants").unwrap(); - doc1.merge(&mut doc2); + doc1.merge(&mut doc2).unwrap(); let reloaded = Automerge::load(&doc1.save().unwrap()).unwrap(); @@ -919,8 +919,8 @@ fn list_counter_del() -> Result<(), automerge::AutomergeError> { doc1.inc(&list, 1, 1)?; doc1.inc(&list, 2, 1)?; - doc1.merge(&mut doc2); - doc1.merge(&mut doc3); + doc1.merge(&mut doc2).unwrap(); + doc1.merge(&mut doc3).unwrap(); let values = doc1.values(&list, 1)?; assert_eq!(values.len(), 3); From e07211278f6c5ccac7fb272fbaaf3f85ecd67f4b Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 24 Feb 2022 18:44:42 -0500 Subject: [PATCH 14/45] v0.0.14 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 88912336..d1e39f12 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.11", + "version": "0.0.14", "license": "MIT", "files": [ "README.md", From c1be06a6c79bb635e55ed29c2067687deaaf44b2 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 28 Feb 2022 19:02:28 -0500 Subject: [PATCH 15/45] blame wip 1 --- automerge/src/automerge.rs | 26 +++++++++++++ automerge/src/clock.rs | 7 ++++ automerge/src/query.rs | 2 + automerge/src/query/blame.rs | 71 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 automerge/src/query/blame.rs diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 5a0ef99d..fb5e4d45 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -315,6 +315,32 @@ impl Automerge { Ok(query.spans) } + pub fn blame( + &self, + obj: &ExId, + base: &[ChangeHash], + points: &[Vec], + ) -> Result, AutomergeError> { + let obj = self.exid_to_obj(obj)?; + let base = self.clock_at(base); + let points: Vec = points.iter().map(|p| self.clock_at(p)).collect(); + let points: Vec = points + .iter() + .enumerate() + .map(|(j, _)| { + points + .iter() + .enumerate() + .filter(|(i, _)| *i != j) + .fold(base.clone(), |acc, (_, c)| acc.union(c)) + }) + .collect(); + + let query = self.ops.search(obj, query::Blame::new(points)); + //Ok(query.points) + unimplemented!() + } + pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; let query = self.ops.search(obj, query::RawSpans::new()); diff --git a/automerge/src/clock.rs b/automerge/src/clock.rs index d01c7748..4d95918d 100644 --- a/automerge/src/clock.rs +++ b/automerge/src/clock.rs @@ -18,6 +18,13 @@ impl Clock { .or_insert(n); } + pub fn union(mut self, other: &Clock) -> Clock { + for (key, val) in &other.0 { + self.include(*key, *val) + } + self + } + pub fn covers(&self, id: &OpId) -> bool { if let Some(val) = self.0.get(&id.1) { val >= &id.0 diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 19ea30ce..e5514ea7 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -7,6 +7,7 @@ use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; +mod blame; mod insert; mod keys; mod keys_at; @@ -23,6 +24,7 @@ mod raw_spans; mod seek_op; mod spans; +pub(crate) use blame::Blame; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; pub(crate) use keys_at::KeysAt; diff --git a/automerge/src/query/blame.rs b/automerge/src/query/blame.rs new file mode 100644 index 00000000..744c19f5 --- /dev/null +++ b/automerge/src/query/blame.rs @@ -0,0 +1,71 @@ +use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; +use crate::types::{ElemId, Op, OpType, ScalarValue}; +use crate::clock::Clock; +use std::collections::HashMap; +use std::fmt::Debug; + +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Blame { + pos: usize, + seen: usize, + last_seen: Option, + last_insert: Option, + seen_at_this_mark: Option, + seen_at_last_mark: Option, + ops: Vec, + points: Vec, + changed: bool, +} + +impl Blame { + pub fn new(points: Vec) -> Self { + Blame { + pos: 0, + seen: 0, + last_seen: None, + last_insert: None, + seen_at_last_mark: None, + seen_at_this_mark: None, + changed: false, + points: Vec::new(), + ops: Vec::new(), + } + } +} + +impl TreeQuery for Blame { + /* + 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 + } +} From b21b59e6a12c77ded73763328a58e2c597dde2d0 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 1 Mar 2022 22:09:21 -0500 Subject: [PATCH 16/45] blame v0.1 --- automerge-wasm/index.d.ts | 16 +++ automerge-wasm/src/interop.rs | 9 ++ automerge-wasm/src/lib.rs | 56 +++++++- automerge-wasm/test/blame.ts | 38 ++++++ automerge/src/autocommit.rs | 9 ++ automerge/src/automerge.rs | 29 ++-- automerge/src/clock.rs | 7 - automerge/src/query.rs | 2 +- automerge/src/query/blame.rs | 127 +++++++++++++----- .../src/transaction/manual_transaction.rs | 9 ++ automerge/src/transaction/transactable.rs | 8 ++ automerge/src/types.rs | 7 + automerge/tests/blame.rs | 39 ++++++ 13 files changed, 290 insertions(+), 66 deletions(-) create mode 100644 automerge-wasm/test/blame.ts create mode 100644 automerge/tests/blame.rs diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 7b97583e..58f2328e 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -61,6 +61,21 @@ export type DecodedChange = { ops: Op[] } +export type ChangeSetAddition = { + start: number, + end: number, +} + +export type ChangeSetDeletion = { + pos: number, + val: string +} + +export type ChangeSet = { + add: ChangeSetAddition[], + del: ChangeSetDeletion[] +} + export type Op = { action: string, obj: ObjID, @@ -102,6 +117,7 @@ export class Automerge { mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; spans(obj: ObjID): any; raw_spans(obj: ObjID): any; + blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet; // transactions commit(message?: string, time?: number): Heads; diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index cf53abf3..4e2a6970 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 7ad1befe..e4c91739 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -13,7 +13,9 @@ mod interop; mod sync; mod value; -use interop::{get_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS}; +use interop::{ + get_heads, get_js_heads, js_get, js_set, map_to_js, to_js_err, to_objtype, to_prop, AR, JS, +}; use sync::SyncState; use value::{datatype, ScalarValue}; @@ -413,6 +415,58 @@ impl Automerge { Ok(result) } + pub fn blame( + &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.blame(&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 save(&mut self) -> Result { self.0 .save() diff --git a/automerge-wasm/test/blame.ts b/automerge-wasm/test/blame.ts new file mode 100644 index 00000000..59d6ba28 --- /dev/null +++ b/automerge-wasm/test/blame.ts @@ -0,0 +1,38 @@ +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('blame', () => { + it('should be able to blame text segments on change sets', () => { + let doc1 = create() + let text = doc1.make("_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 blame = doc1.blame(text, h1, [h2, h3]) + + assert.deepEqual(blame, [ + { add: [ { start: 11, end: 15 } ], del: [ { pos: 15, val: ' little' } ] }, + { add: [ { start: 0, end: 6 } ], del: [] } + ]) + }) + }) +}) diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index a5673a57..84ea682b 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -431,6 +431,15 @@ impl Transactable for AutoCommit { self.doc.raw_spans(obj) } + fn blame( + &self, + obj: &ExId, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.blame(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 fb5e4d45..eb64b0a2 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -318,27 +318,16 @@ impl Automerge { pub fn blame( &self, obj: &ExId, - base: &[ChangeHash], - points: &[Vec], - ) -> Result, AutomergeError> { + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj)?; - let base = self.clock_at(base); - let points: Vec = points.iter().map(|p| self.clock_at(p)).collect(); - let points: Vec = points - .iter() - .enumerate() - .map(|(j, _)| { - points - .iter() - .enumerate() - .filter(|(i, _)| *i != j) - .fold(base.clone(), |acc, (_, c)| acc.union(c)) - }) - .collect(); - - let query = self.ops.search(obj, query::Blame::new(points)); - //Ok(query.points) - unimplemented!() + let baseline = self.clock_at(baseline); + let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); + let query = self + .ops + .search(obj, query::Blame::new(baseline, change_sets)); + Ok(query.change_sets) } pub fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError> { diff --git a/automerge/src/clock.rs b/automerge/src/clock.rs index 4d95918d..d01c7748 100644 --- a/automerge/src/clock.rs +++ b/automerge/src/clock.rs @@ -18,13 +18,6 @@ impl Clock { .or_insert(n); } - pub fn union(mut self, other: &Clock) -> Clock { - for (key, val) in &other.0 { - self.include(*key, *val) - } - self - } - pub fn covers(&self, id: &OpId) -> bool { if let Some(val) = self.0.get(&id.1) { val >= &id.0 diff --git a/automerge/src/query.rs b/automerge/src/query.rs index e5514ea7..52a8e286 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -24,7 +24,7 @@ mod raw_spans; mod seek_op; mod spans; -pub(crate) use blame::Blame; +pub(crate) use blame::{Blame, ChangeSet}; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; pub(crate) use keys_at::KeysAt; diff --git a/automerge/src/query/blame.rs b/automerge/src/query/blame.rs index 744c19f5..a1ce2dd1 100644 --- a/automerge/src/query/blame.rs +++ b/automerge/src/query/blame.rs @@ -1,8 +1,8 @@ -use crate::query::{OpSetMetadata, QueryResult, TreeQuery}; -use crate::types::{ElemId, Op, OpType, ScalarValue}; use crate::clock::Clock; -use std::collections::HashMap; +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 Blame { @@ -10,60 +10,113 @@ pub(crate) struct Blame { seen: usize, last_seen: Option, last_insert: Option, - seen_at_this_mark: Option, - seen_at_last_mark: Option, - ops: Vec, - points: Vec, - changed: bool, + 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 Blame { - pub fn new(points: Vec) -> Self { + pub fn new(baseline: Clock, change_sets: Vec) -> Self { Blame { pos: 0, seen: 0, last_seen: None, last_insert: None, - seen_at_last_mark: None, - seen_at_this_mark: None, - changed: false, - points: Vec::new(), - ops: Vec::new(), + 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().all(|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(); } } } impl TreeQuery for Blame { - /* - 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()); - } - } + fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { if element.insert { self.last_seen = None; self.last_insert = element.elemid(); } if self.last_seen.is_none() && element.visible() { - //self.check_marks(); + self.update_add(element); self.seen += 1; self.last_seen = element.elemid(); - self.seen_at_this_mark = element.elemid(); + } + if !element.succ.is_empty() { + self.update_del(element); } self.pos += 1; QueryResult::Next diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index cd341702..ac339ec4 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -201,6 +201,15 @@ impl<'a> Transactable for Transaction<'a> { self.doc.raw_spans(obj) } + fn blame( + &self, + obj: &ExId, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError> { + self.doc.blame(obj, baseline, change_sets) + } + fn value>( &self, obj: &ExId, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index 5e4b21f5..eda850a8 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -115,6 +115,14 @@ pub trait Transactable { /// test raw_spans api for mark/span experiment fn raw_spans(&self, obj: &ExId) -> Result, AutomergeError>; + /// test blame api for mark/span experiment + fn blame( + &self, + obj: &ExId, + baseline: &[ChangeHash], + change_sets: &[Vec], + ) -> Result, AutomergeError>; + /// Get the value at this prop in the object. fn value>( &self, diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 54192908..53518e59 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -456,6 +456,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), diff --git a/automerge/tests/blame.rs b/automerge/tests/blame.rs new file mode 100644 index 00000000..65e9ca60 --- /dev/null +++ b/automerge/tests/blame.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_blame_text() -> Result<(), AutomergeError> { + let mut doc = AutoCommit::new(); + let note = doc.set(&ROOT, "note", automerge::Value::text())?.unwrap(); + 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 blame = doc.blame(¬e, &baseline, &cs)?; + assert!(&text[blame[0].add[0].clone()] == " big"); + assert!(blame[0].del[0] == (15, " little".to_owned())); + //println!("{:?} == {:?}", blame[0].del[0] , (15, " little".to_owned())); + assert!(&text[blame[1].add[0].clone()] == "Well, "); + //println!("- ------- blame = {:?}", blame); + Ok(()) +} From 42b6ffe9d80160af4e2ab1b61693630dbf29d35d Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 2 Mar 2022 09:33:04 -0500 Subject: [PATCH 17/45] v0.0.15 --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index d1e39f12..05e37c9a 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.14", + "version": "0.0.15", "license": "MIT", "files": [ "README.md", From 4c11c865324c3fca84ec285d000d86681f267dc2 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 2 Mar 2022 10:27:54 -0500 Subject: [PATCH 18/45] v0.0.16 - properly blame items deleted by both --- automerge-wasm/index.d.ts | 2 +- automerge-wasm/package.json | 2 +- automerge-wasm/test/blame.ts | 47 +++++++++++++++++++++++++++++++++++- automerge/src/automerge.rs | 3 ++- automerge/src/query/blame.rs | 11 +++++++-- 5 files changed, 59 insertions(+), 6 deletions(-) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 58f2328e..aa7e59bb 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -117,7 +117,7 @@ export class Automerge { mark(obj: ObjID, name: string, range: string, value: Value, datatype?: Datatype): void; spans(obj: ObjID): any; raw_spans(obj: ObjID): any; - blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet; + blame(obj: ObjID, baseline: Heads, changeset: Heads[]): ChangeSet[]; // transactions commit(message?: string, time?: number): Heads; diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 05e37c9a..2044a3b6 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.15", + "version": "0.0.16", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/test/blame.ts b/automerge-wasm/test/blame.ts index 59d6ba28..a2500ba4 100644 --- a/automerge-wasm/test/blame.ts +++ b/automerge-wasm/test/blame.ts @@ -8,7 +8,7 @@ import { DecodedSyncMessage, Hash } from '..' describe('Automerge', () => { describe('blame', () => { - it('should be able to blame text segments on change sets', () => { + it.only('should be able to blame text segments on change sets', () => { let doc1 = create() let text = doc1.make("_root", "notes","hello little world") let h1 = doc1.getHeads(); @@ -34,5 +34,50 @@ describe('Automerge', () => { { add: [ { start: 0, end: 6 } ], del: [] } ]) }) + + it.only('should be able to hand complex blame change sets', () => { + let doc1 = create("aaaa") + let text = doc1.make("_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.blame(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.blame(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' } ] } + ]) + }) }) }) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index eb64b0a2..7a87e2e8 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -324,9 +324,10 @@ impl Automerge { let obj = self.exid_to_obj(obj)?; let baseline = self.clock_at(baseline); let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); - let query = self + let mut query = self .ops .search(obj, query::Blame::new(baseline, change_sets)); + query.finish(); Ok(query.change_sets) } diff --git a/automerge/src/query/blame.rs b/automerge/src/query/blame.rs index a1ce2dd1..d6ed6856 100644 --- a/automerge/src/query/blame.rs +++ b/automerge/src/query/blame.rs @@ -87,7 +87,7 @@ impl Blame { 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().all(|id| cs.clock.covers(id)) { + 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 { @@ -97,9 +97,16 @@ impl Blame { } } } else { - cs.cut_del(); + //cs.cut_del(); } + //cs.cut_add(); + } + } + + pub fn finish(&mut self) { + for cs in &mut self.change_sets { cs.cut_add(); + cs.cut_del(); } } } From d195a81d491c8ffd510c9dc28617099e409efccc Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 2 Mar 2022 18:35:58 -0500 Subject: [PATCH 19/45] v17 --release --- automerge-wasm/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 2044a3b6..74ec4790 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.16", + "version": "0.0.17", "license": "MIT", "files": [ "README.md", @@ -22,8 +22,8 @@ "main": "./node/index.js", "scripts": { "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 --dev --out-name index -d web && cp index.d.ts web", - "release-n": "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" }, From dbbdd616fde3e73935fa6f7199c3a60b4163987d Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Fri, 4 Mar 2022 14:16:06 -0500 Subject: [PATCH 20/45] clippy/fmt --- automerge-wasm/test/blame.ts | 4 ++-- automerge/src/automerge.rs | 5 ++++- automerge/src/transaction/manual_transaction.rs | 2 +- automerge/src/visualisation.rs | 2 ++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/automerge-wasm/test/blame.ts b/automerge-wasm/test/blame.ts index a2500ba4..c5b02c98 100644 --- a/automerge-wasm/test/blame.ts +++ b/automerge-wasm/test/blame.ts @@ -8,7 +8,7 @@ import { DecodedSyncMessage, Hash } from '..' describe('Automerge', () => { describe('blame', () => { - it.only('should be able to blame text segments on change sets', () => { + it('should be able to blame text segments on change sets', () => { let doc1 = create() let text = doc1.make("_root", "notes","hello little world") let h1 = doc1.getHeads(); @@ -35,7 +35,7 @@ describe('Automerge', () => { ]) }) - it.only('should be able to hand complex blame change sets', () => { + it('should be able to hand complex blame change sets', () => { let doc1 = create("aaaa") let text = doc1.make("_root", "notes","AAAAAA") let h1 = doc1.getHeads(); diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index afc177ad..c063b94d 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -331,7 +331,10 @@ impl Automerge { Ok(query.change_sets) } - pub fn raw_spans>(&self, obj: O) -> Result, AutomergeError> { + 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 diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index fcf5e605..7abbf0a5 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::AutomergeError; -use crate::{query, Automerge, ChangeHash, Prop, ScalarValue, Value, Keys, KeysAt}; +use crate::{query, Automerge, ChangeHash, Keys, KeysAt, Prop, ScalarValue, Value}; use super::{CommitOptions, Transactable, TransactionInner}; diff --git a/automerge/src/visualisation.rs b/automerge/src/visualisation.rs index 442c9eb4..f8ac9839 100644 --- a/automerge/src/visualisation.rs +++ b/automerge/src/visualisation.rs @@ -234,6 +234,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(), From 42446fa5c2ef6a513361146948fd8a95a874f447 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 7 Mar 2022 13:45:56 -0500 Subject: [PATCH 21/45] blame -> attribute --- automerge-wasm/src/lib.rs | 7 ++++--- automerge/src/autocommit.rs | 4 ++-- automerge/src/automerge.rs | 4 ++-- automerge/src/query.rs | 4 ++-- automerge/src/query/{blame.rs => attribute.rs} | 11 ++++------- automerge/src/transaction/manual_transaction.rs | 4 ++-- automerge/src/transaction/transactable.rs | 4 ++-- automerge/tests/{blame.rs => attribute.rs} | 14 +++++++------- 8 files changed, 25 insertions(+), 27 deletions(-) rename automerge/src/query/{blame.rs => attribute.rs} (92%) rename automerge/tests/{blame.rs => attribute.rs} (68%) diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index f9b7ae8c..58f346c1 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -226,7 +226,6 @@ impl Automerge { Ok(()) } - // FIXME remove this later pub fn make( &mut self, obj: JsValue, @@ -234,6 +233,8 @@ impl Automerge { 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, datatype) } @@ -455,7 +456,7 @@ impl Automerge { Ok(result) } - pub fn blame( + pub fn attribute( &mut self, obj: JsValue, baseline: JsValue, @@ -468,7 +469,7 @@ impl Automerge { .iter() .map(get_js_heads) .collect::, _>>()?; - let result = self.0.blame(&obj, &baseline, &change_sets)?; + let result = self.0.attribute(&obj, &baseline, &change_sets)?; let result = result .into_iter() .map(|cs| { diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index 02dbae01..115ab499 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -457,13 +457,13 @@ impl Transactable for AutoCommit { self.doc.raw_spans(obj) } - fn blame>( + fn attribute>( &self, obj: O, baseline: &[ChangeHash], change_sets: &[Vec], ) -> Result, AutomergeError> { - self.doc.blame(obj, baseline, change_sets) + self.doc.attribute(obj, baseline, change_sets) } // TODO - I need to return these OpId's here **only** to get diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index a7c622d1..cfa136c8 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -315,7 +315,7 @@ impl Automerge { Ok(query.spans) } - pub fn blame>( + pub fn attribute>( &self, obj: O, baseline: &[ChangeHash], @@ -326,7 +326,7 @@ impl Automerge { let change_sets: Vec = change_sets.iter().map(|p| self.clock_at(p)).collect(); let mut query = self .ops - .search(obj, query::Blame::new(baseline, change_sets)); + .search(obj, query::Attribute::new(baseline, change_sets)); query.finish(); Ok(query.change_sets) } diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 52a8e286..9381e83a 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -7,7 +7,7 @@ use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; -mod blame; +mod attribute; mod insert; mod keys; mod keys_at; @@ -24,7 +24,7 @@ mod raw_spans; mod seek_op; mod spans; -pub(crate) use blame::{Blame, ChangeSet}; +pub(crate) use attribute::{Attribute, ChangeSet}; pub(crate) use insert::InsertNth; pub(crate) use keys::Keys; pub(crate) use keys_at::KeysAt; diff --git a/automerge/src/query/blame.rs b/automerge/src/query/attribute.rs similarity index 92% rename from automerge/src/query/blame.rs rename to automerge/src/query/attribute.rs index d6ed6856..72415483 100644 --- a/automerge/src/query/blame.rs +++ b/automerge/src/query/attribute.rs @@ -5,11 +5,10 @@ use std::fmt::Debug; use std::ops::Range; #[derive(Debug, Clone, PartialEq)] -pub(crate) struct Blame { +pub(crate) struct Attribute { pos: usize, seen: usize, last_seen: Option, - last_insert: Option, baseline: Clock, pub change_sets: Vec, } @@ -49,13 +48,12 @@ impl ChangeSet { } } -impl Blame { +impl Attribute { pub fn new(baseline: Clock, change_sets: Vec) -> Self { - Blame { + Attribute { pos: 0, seen: 0, last_seen: None, - last_insert: None, baseline, change_sets: change_sets.into_iter().map(|c| c.into()).collect(), } @@ -111,11 +109,10 @@ impl Blame { } } -impl TreeQuery for Blame { +impl TreeQuery for Attribute { fn query_element_with_metadata(&mut self, element: &Op, _m: &OpSetMetadata) -> QueryResult { if element.insert { self.last_seen = None; - self.last_insert = element.elemid(); } if self.last_seen.is_none() && element.visible() { self.update_add(element); diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index 515c7856..d8cdde89 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -242,13 +242,13 @@ impl<'a> Transactable for Transaction<'a> { self.doc.raw_spans(obj) } - fn blame>( + fn attribute>( &self, obj: O, baseline: &[ChangeHash], change_sets: &[Vec], ) -> Result, AutomergeError> { - self.doc.blame(obj, baseline, change_sets) + self.doc.attribute(obj, baseline, change_sets) } fn value, P: Into>( diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index f4538857..afa6a8b1 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -144,8 +144,8 @@ pub trait Transactable { /// test raw_spans api for mark/span experiment fn raw_spans>(&self, obj: O) -> Result, AutomergeError>; - /// test blame api for mark/span experiment - fn blame>( + /// test attribute api for mark/span experiment + fn attribute>( &self, obj: O, baseline: &[ChangeHash], diff --git a/automerge/tests/blame.rs b/automerge/tests/attribute.rs similarity index 68% rename from automerge/tests/blame.rs rename to automerge/tests/attribute.rs index 7449028b..c2996656 100644 --- a/automerge/tests/blame.rs +++ b/automerge/tests/attribute.rs @@ -10,7 +10,7 @@ use helpers::{ */ #[test] -fn simple_blame_text() -> Result<(), AutomergeError> { +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")?; @@ -29,11 +29,11 @@ fn simple_blame_text() -> Result<(), AutomergeError> { let text = doc.text(¬e)?; assert!(text == "Well, hello big world"); let cs = vec![h2, h3]; - let blame = doc.blame(¬e, &baseline, &cs)?; - assert!(&text[blame[0].add[0].clone()] == " big"); - assert!(blame[0].del[0] == (15, " little".to_owned())); - //println!("{:?} == {:?}", blame[0].del[0] , (15, " little".to_owned())); - assert!(&text[blame[1].add[0].clone()] == "Well, "); - //println!("- ------- blame = {:?}", blame); + 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(()) } From 4094e82f04b30b8f59778db1eb459d8ee6a49e5a Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 7 Mar 2022 13:48:22 -0500 Subject: [PATCH 22/45] rename tests to attribute --- automerge-wasm/index.d.ts | 1 + automerge-wasm/src/lib.rs | 10 ++++++++++ automerge-wasm/test/{blame.ts => attribute.ts} | 14 +++++++------- 3 files changed, 18 insertions(+), 7 deletions(-) rename automerge-wasm/test/{blame.ts => attribute.ts} (86%) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 199d094c..0fbb90cb 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -121,6 +121,7 @@ export class Automerge { 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[]; // transactions commit(message?: string, time?: number): Heads; diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 58f346c1..4e145ce1 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -456,6 +456,16 @@ impl Automerge { 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, diff --git a/automerge-wasm/test/blame.ts b/automerge-wasm/test/attribute.ts similarity index 86% rename from automerge-wasm/test/blame.ts rename to automerge-wasm/test/attribute.ts index c5b02c98..db1880c2 100644 --- a/automerge-wasm/test/blame.ts +++ b/automerge-wasm/test/attribute.ts @@ -7,8 +7,8 @@ import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, init import { DecodedSyncMessage, Hash } from '..' describe('Automerge', () => { - describe('blame', () => { - it('should be able to blame text segments on change sets', () => { + describe('attribute', () => { + it('should be able to attribute text segments on change sets', () => { let doc1 = create() let text = doc1.make("_root", "notes","hello little world") let h1 = doc1.getHeads(); @@ -27,15 +27,15 @@ describe('Automerge', () => { doc1.merge(doc2) doc1.merge(doc3) assert.deepEqual(doc1.text(text), "Well, hello big world") - let blame = doc1.blame(text, h1, [h2, h3]) + let attribute = doc1.attribute(text, h1, [h2, h3]) - assert.deepEqual(blame, [ + 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 blame change sets', () => { + it('should be able to hand complex attribute change sets', () => { let doc1 = create("aaaa") let text = doc1.make("_root", "notes","AAAAAA") let h1 = doc1.getHeads(); @@ -64,7 +64,7 @@ describe('Automerge', () => { doc1.merge(doc2) - assert.deepEqual(doc1.blame(text, h1, [h2]), [ + assert.deepEqual(doc1.attribute(text, h1, [h2]), [ { add: [ {start:0, end: 4}, { start: 6, end: 8 } ], del: [ { pos: 4, val: 'AAAA' } ] }, ]) @@ -74,7 +74,7 @@ describe('Automerge', () => { // with tombstones its // BBBB.C..C.AC.BB - assert.deepEqual(doc1.blame(text, h1, [h2,h3]), [ + 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' } ] } ]) From cbf1ac03b2b5a1e11dd102e7b791a6814d353cf5 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 8 Mar 2022 12:00:02 -0500 Subject: [PATCH 23/45] added attribute2() - janky version --- automerge-wasm/index.d.ts | 3 ++ automerge-wasm/package.json | 2 +- automerge-wasm/src/lib.rs | 54 +++++++++++++++++++ automerge-wasm/test/attribute.ts | 34 +++++++++++- automerge/src/autocommit.rs | 14 +++++ automerge/src/automerge.rs | 16 ++++++ automerge/src/query.rs | 2 + .../src/transaction/manual_transaction.rs | 9 ++++ automerge/src/transaction/transactable.rs | 8 +++ 9 files changed, 139 insertions(+), 3 deletions(-) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 0fbb90cb..d778504e 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -63,11 +63,13 @@ export type DecodedChange = { } export type ChangeSetAddition = { + actor: string, start: number, end: number, } export type ChangeSetDeletion = { + actor: string, pos: number, val: string } @@ -122,6 +124,7 @@ export class Automerge { 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; diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 74ec4790..21ea5b0d 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.17", + "version": "0.0.18", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 4e145ce1..a6d57c8d 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -518,6 +518,60 @@ impl Automerge { 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()) } diff --git a/automerge-wasm/test/attribute.ts b/automerge-wasm/test/attribute.ts index db1880c2..50aff286 100644 --- a/automerge-wasm/test/attribute.ts +++ b/automerge-wasm/test/attribute.ts @@ -10,7 +10,7 @@ describe('Automerge', () => { describe('attribute', () => { it('should be able to attribute text segments on change sets', () => { let doc1 = create() - let text = doc1.make("_root", "notes","hello little world") + let text = doc1.set_object("_root", "notes","hello little world") let h1 = doc1.getHeads(); let doc2 = doc1.fork(); @@ -37,7 +37,7 @@ describe('Automerge', () => { it('should be able to hand complex attribute change sets', () => { let doc1 = create("aaaa") - let text = doc1.make("_root", "notes","AAAAAA") + let text = doc1.set_object("_root", "notes","AAAAAA") let h1 = doc1.getHeads(); let doc2 = doc1.fork("bbbb"); @@ -80,4 +80,34 @@ describe('Automerge', () => { ]) }) }) + 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: [] } + ]) + }) + }) }) diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index 115ab499..4e139765 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -28,6 +28,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 { @@ -466,6 +471,15 @@ impl Transactable for AutoCommit { 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 cfa136c8..16a839d1 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -331,6 +331,22 @@ impl Automerge { 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, diff --git a/automerge/src/query.rs b/automerge/src/query.rs index 9381e83a..e58bd9a6 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -8,6 +8,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::Debug; mod attribute; +mod attribute2; mod insert; mod keys; mod keys_at; @@ -25,6 +26,7 @@ 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; diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index d8cdde89..684df179 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -251,6 +251,15 @@ impl<'a> Transactable for Transaction<'a> { 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 afa6a8b1..480d1357 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -152,6 +152,14 @@ pub trait Transactable { 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, From 22b62b14b5a9463faa6d39a75ba2275daf007505 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 8 Mar 2022 12:03:31 -0500 Subject: [PATCH 24/45] forgot to add the new file --- automerge/src/query/attribute2.rs | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 automerge/src/query/attribute2.rs 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 + } +} From b81e0fd619eb1ee377f724542bf623fa216f363e Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 7 Mar 2022 11:46:25 -0500 Subject: [PATCH 25/45] update wasm test for set_object --- automerge-wasm/index.d.ts | 10 +++++----- automerge-wasm/src/lib.rs | 25 +++++++++---------------- automerge-wasm/test/test.ts | 6 ------ 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index d778504e..78151038 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -100,12 +100,12 @@ export function decodeSyncState(data: Uint8Array): SyncState; export class Automerge { // change state - set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID | undefined; + set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): undefined; set_object(obj: ObjID, prop: Prop, value: ObjType): ObjID; - make(obj: ObjID, prop: Prop, value: ObjType): ObjID; - insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): ObjID | undefined; - insert_object(obj: ObjID, index: number, value: ObjType): ObjID | undefined; - push(obj: ObjID, value: Value, datatype?: Datatype): ObjID | undefined; + insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): undefined; + insert_object(obj: ObjID, index: number, value: ObjType): ObjID; + push(obj: ObjID, value: Value, datatype?: Datatype): undefined; + push_object(obj: ObjID, value: ObjType): ObjID; splice(obj: ObjID, start: number, delete_count: number, text?: string | Array): ObjID[] | undefined; inc(obj: ObjID, prop: Prop, value: number): void; del(obj: ObjID, prop: Prop): void; diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index a6d57c8d..bd38c543 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -163,15 +163,10 @@ impl Automerge { Ok(()) } - pub fn push_object( - &mut self, - obj: JsValue, - value: JsValue, - datatype: JsValue, - ) -> Result, JsValue> { + pub fn push_object(&mut self, obj: JsValue, value: JsValue) -> Result, JsValue> { let obj = self.import(obj)?; - let (value, subvals) = to_objtype(&value, &datatype.as_string()) - .ok_or_else(|| to_js_err("expected object"))?; + let (value, subvals) = + to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let index = self.0.length(&obj); let opid = self.0.insert_object(&obj, index, value)?; self.subset(&opid, subvals)?; @@ -199,12 +194,11 @@ impl Automerge { obj: JsValue, index: f64, value: JsValue, - datatype: JsValue, ) -> Result, JsValue> { let obj = self.import(obj)?; let index = index as f64; - let (value, subvals) = to_objtype(&value, &datatype.as_string()) - .ok_or_else(|| to_js_err("expected object"))?; + let (value, subvals) = + to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let opid = self.0.insert_object(&obj, index as usize, value)?; self.subset(&opid, subvals)?; Ok(opid.to_string().into()) @@ -231,11 +225,11 @@ impl Automerge { obj: JsValue, prop: JsValue, value: JsValue, - datatype: 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, datatype) + self.set_object(obj, prop, value) } pub fn set_object( @@ -243,12 +237,11 @@ impl Automerge { obj: JsValue, prop: JsValue, value: JsValue, - datatype: JsValue, ) -> Result { let obj = self.import(obj)?; let prop = self.import_prop(prop)?; - let (value, subvals) = to_objtype(&value, &datatype.as_string()) - .ok_or_else(|| to_js_err("expected object"))?; + let (value, subvals) = + to_objtype(&value, &None).ok_or_else(|| to_js_err("expected object"))?; let opid = self.0.set_object(&obj, prop, value)?; self.subset(&opid, subvals)?; Ok(opid.to_string().into()) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 3c51f7a2..029505a1 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -124,7 +124,6 @@ describe('Automerge', () => { let result let submap = doc.set_object(root, "submap", {}) - if (!submap) throw new Error('should be not null') doc.set(submap, "number", 6, "uint") assert.strictEqual(doc.pendingOps(),2) @@ -327,7 +326,6 @@ describe('Automerge', () => { it('local inc increments all visible counters in a sequence', () => { let doc1 = create("aaaa") let seq = doc1.set_object("_root", "seq", []) - if (!seq) throw new Error('Should not be undefined') doc1.insert(seq, 0, "hello") let doc2 = loadDoc(doc1.save(), "bbbb"); let doc3 = loadDoc(doc1.save(), "cccc"); @@ -400,11 +398,8 @@ describe('Automerge', () => { it('objects without properties are preserved', () => { let doc1 = create("aaaa") let a = doc1.set_object("_root","a",{}); - if (!a) throw new Error('should not be undefined') let b = doc1.set_object("_root","b",{}); - if (!b) throw new Error('should not be undefined') let c = doc1.set_object("_root","c",{}); - if (!c) throw new Error('should not be undefined') let d = doc1.set(c,"d","dd"); let saved = doc1.save(); let doc2 = loadDoc(saved); @@ -496,7 +491,6 @@ describe('Automerge', () => { // make changes for n1 that n2 should request let list = n1.set_object("_root","n",[]) - if (!list) throw new Error('undefined') n1.commit("",0) for (let i = 0; i < 10; i++) { n1.insert(list, i, i) From 304195d7206035af04feb9c117908978654f122f Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 11:14:21 +0000 Subject: [PATCH 26/45] Fix typo on QueryResult --- automerge/src/op_tree.rs | 4 ++-- automerge/src/query.rs | 4 ++-- automerge/src/query/insert.rs | 2 +- automerge/src/query/nth.rs | 2 +- automerge/src/query/opid.rs | 2 +- automerge/src/query/seek_op.rs | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/automerge/src/op_tree.rs b/automerge/src/op_tree.rs index 5503d164..903cfde6 100644 --- a/automerge/src/op_tree.rs +++ b/automerge/src/op_tree.rs @@ -56,7 +56,7 @@ impl OpTreeInternal { self.root_node .as_ref() .map(|root| match query.query_node_with_metadata(root, m) { - QueryResult::Decend => root.search(&mut query, m), + QueryResult::Descend => root.search(&mut query, m), _ => true, }); query @@ -199,7 +199,7 @@ impl OpTreeNode { } else { for (child_index, child) in self.children.iter().enumerate() { match query.query_node_with_metadata(child, m) { - QueryResult::Decend => { + QueryResult::Descend => { if child.search(query, m) { return true; } diff --git a/automerge/src/query.rs b/automerge/src/query.rs index e58bd9a6..c34af84b 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -73,7 +73,7 @@ pub(crate) trait TreeQuery { } fn query_node(&mut self, _child: &OpTreeNode) -> QueryResult { - QueryResult::Decend + QueryResult::Descend } #[inline(always)] @@ -89,7 +89,7 @@ pub(crate) trait TreeQuery { #[derive(Debug, Clone, PartialEq)] pub(crate) enum QueryResult { Next, - Decend, + Descend, Finish, } diff --git a/automerge/src/query/insert.rs b/automerge/src/query/insert.rs index 38a58e45..c72a6e61 100644 --- a/automerge/src/query/insert.rs +++ b/automerge/src/query/insert.rs @@ -64,7 +64,7 @@ impl TreeQuery for InsertNth { num_vis -= 1; } if self.seen + num_vis >= self.target { - QueryResult::Decend + QueryResult::Descend } else { self.n += child.len(); self.seen += num_vis; diff --git a/automerge/src/query/nth.rs b/automerge/src/query/nth.rs index 8d692a76..b62410e4 100644 --- a/automerge/src/query/nth.rs +++ b/automerge/src/query/nth.rs @@ -48,7 +48,7 @@ impl TreeQuery for Nth { num_vis -= 1; } if self.seen + num_vis > self.target { - QueryResult::Decend + QueryResult::Descend } else { self.pos += child.len(); self.seen += num_vis; diff --git a/automerge/src/query/opid.rs b/automerge/src/query/opid.rs index fc2132f2..2a68ad1c 100644 --- a/automerge/src/query/opid.rs +++ b/automerge/src/query/opid.rs @@ -33,7 +33,7 @@ impl OpIdSearch { impl TreeQuery for OpIdSearch { fn query_node(&mut self, child: &OpTreeNode) -> QueryResult { if child.index.ops.contains(&self.target) { - QueryResult::Decend + QueryResult::Descend } else { self.pos += child.len(); QueryResult::Next diff --git a/automerge/src/query/seek_op.rs b/automerge/src/query/seek_op.rs index 9aba4b46..a2f3b750 100644 --- a/automerge/src/query/seek_op.rs +++ b/automerge/src/query/seek_op.rs @@ -49,7 +49,7 @@ impl TreeQuery for SeekOp { m: &OpSetMetadata, ) -> QueryResult { if self.found { - return QueryResult::Decend; + return QueryResult::Descend; } match self.op.key { Key::Seq(e) if e == HEAD => { @@ -67,7 +67,7 @@ impl TreeQuery for SeekOp { } Key::Seq(e) => { if self.found || child.index.ops.contains(&e.0) { - QueryResult::Decend + QueryResult::Descend } else { self.pos += child.len(); QueryResult::Next From 8f4c1fc2096c20742fe4e33cb806426d979b509f Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Fri, 4 Mar 2022 17:30:19 +0000 Subject: [PATCH 27/45] Add failing tests for deleting nothing --- automerge/src/automerge.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 16a839d1..90f9fde9 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -1321,4 +1321,28 @@ mod tests { assert_eq!(doc.value(&map1, "c").unwrap().unwrap().0, Value::int(3)); } + + #[test] + fn delete_nothing_returns_error_map() { + let mut doc = Automerge::new(); + let mut tx = doc.transaction(); + assert!(tx.del(ROOT, "a").is_err()); + // not an error currently so breaks loading + tx.commit(); + + let bytes = doc.save().unwrap(); + assert!(Automerge::load(&bytes).is_err()); + } + + #[test] + fn delete_nothing_returns_error_list() { + let mut doc = Automerge::new(); + let mut tx = doc.transaction(); + assert!(tx.del(ROOT, 0).is_err()); + // not an error currently so breaks loading + tx.commit(); + + let bytes = doc.save().unwrap(); + assert!(Automerge::load(&bytes).is_err()); + } } From d02737ad128558b0a7f350f2e0e48452ee74318a Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Tue, 8 Mar 2022 17:42:03 +0000 Subject: [PATCH 28/45] Fix del missing key in map --- automerge/src/automerge.rs | 20 ++++++++++++++++---- automerge/src/transaction/inner.rs | 5 +++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 90f9fde9..7ea222a5 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -1326,23 +1326,35 @@ mod tests { fn delete_nothing_returns_error_map() { let mut doc = Automerge::new(); let mut tx = doc.transaction(); - assert!(tx.del(ROOT, "a").is_err()); + assert!(tx.del(ROOT, "a").is_ok()); // not an error currently so breaks loading tx.commit(); + dbg!(doc.get_last_local_change()); let bytes = doc.save().unwrap(); - assert!(Automerge::load(&bytes).is_err()); + assert!(Automerge::load(&bytes).is_ok()); + + let mut tx = doc.transaction(); + tx.set(ROOT, "a", 1).unwrap(); + tx.commit(); + + let mut tx = doc.transaction(); + // a real op + tx.del(ROOT, "a").unwrap(); + // a no-op + tx.del(ROOT, "a").unwrap(); + tx.commit(); } #[test] fn delete_nothing_returns_error_list() { let mut doc = Automerge::new(); let mut tx = doc.transaction(); - assert!(tx.del(ROOT, 0).is_err()); + tx.del(ROOT, 0).unwrap(); // not an error currently so breaks loading tx.commit(); let bytes = doc.save().unwrap(); - assert!(Automerge::load(&bytes).is_err()); + assert!(Automerge::load(&bytes).is_ok()); } } diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index 6d9a9456..f175dbb6 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -251,6 +251,11 @@ impl TransactionInner { let prop = doc.ops.m.props.cache(prop); let query = doc.ops.search(obj, query::Prop::new(prop)); + // no key present to delete + if query.ops.is_empty() && action == OpType::Del { + return Ok(None); + } + if query.ops.len() == 1 && query.ops[0].is_noop(&action) { return Ok(None); } From 1b1d50dfaf5ea21022a3461dbd212530798c94c9 Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 09:38:03 +0000 Subject: [PATCH 29/45] Update delete nothing tests --- automerge/src/automerge.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 7ea222a5..4345c2fb 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -1323,13 +1323,14 @@ mod tests { } #[test] - fn delete_nothing_returns_error_map() { + fn delete_nothing_in_map_is_noop() { let mut doc = Automerge::new(); let mut tx = doc.transaction(); + // deleting a missing key in a map should just be a noop assert!(tx.del(ROOT, "a").is_ok()); - // not an error currently so breaks loading tx.commit(); - dbg!(doc.get_last_local_change()); + let last_change = doc.get_last_local_change().unwrap(); + assert_eq!(last_change.len(), 0); let bytes = doc.save().unwrap(); assert!(Automerge::load(&bytes).is_ok()); @@ -1337,6 +1338,8 @@ mod tests { let mut tx = doc.transaction(); tx.set(ROOT, "a", 1).unwrap(); tx.commit(); + let last_change = doc.get_last_local_change().unwrap(); + assert_eq!(last_change.len(), 1); let mut tx = doc.transaction(); // a real op @@ -1344,17 +1347,15 @@ mod tests { // a no-op tx.del(ROOT, "a").unwrap(); tx.commit(); + let last_change = doc.get_last_local_change().unwrap(); + assert_eq!(last_change.len(), 1); } #[test] - fn delete_nothing_returns_error_list() { + fn delete_nothing_in_list_returns_error() { let mut doc = Automerge::new(); let mut tx = doc.transaction(); - tx.del(ROOT, 0).unwrap(); - // not an error currently so breaks loading - tx.commit(); - - let bytes = doc.save().unwrap(); - assert!(Automerge::load(&bytes).is_ok()); + // deleting an element in a list that does not exist is an error + assert!(tx.del(ROOT, 0).is_err()); } } From 63b4c96e71063eaed8bddcd8ec0ccf01a2b944ae Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 10:33:57 +0000 Subject: [PATCH 30/45] Update save call --- automerge/src/automerge.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 4345c2fb..b74df9dd 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -1332,7 +1332,7 @@ mod tests { let last_change = doc.get_last_local_change().unwrap(); assert_eq!(last_change.len(), 0); - let bytes = doc.save().unwrap(); + let bytes = doc.save(); assert!(Automerge::load(&bytes).is_ok()); let mut tx = doc.transaction(); From aad4852e3050f8405bd336235136ee202dca1586 Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 12:33:20 +0000 Subject: [PATCH 31/45] Misc API updates - Commit now returns just a single hash rather than a vec. Since the change we create from committing has all of the heads as deps there can only be one hash/head after committing. - Apply changes now takes a Vec rather than a slice. This avoids having to clone them inside. - transact_with now passes the result of the closure to the commit options function - Remove patch struct - Change receive_sync_message to return a () instead of the `Option` - Change `Transaction*` structs to just `*` and use the transaction module - Make CommitOptions fields public --- automerge-wasm/src/lib.rs | 12 ++-- automerge/examples/quickstart.rs | 8 +-- automerge/src/autocommit.rs | 21 +++---- automerge/src/automerge.rs | 50 ++++++++--------- automerge/src/sync.rs | 9 +-- automerge/src/transaction.rs | 6 +- automerge/src/transaction/commit.rs | 4 +- automerge/src/transaction/inner.rs | 10 ++-- .../src/transaction/manual_transaction.rs | 4 +- automerge/src/transaction/result.rs | 55 ++++--------------- automerge/src/types.rs | 3 - 11 files changed, 67 insertions(+), 115 deletions(-) diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index bd38c543..04da499a 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -75,7 +75,7 @@ impl Automerge { (self.0.pending_ops() as u32).into() } - pub fn commit(&mut self, message: Option, time: Option) -> Array { + pub fn commit(&mut self, message: Option, time: Option) -> JsValue { let mut commit_opts = CommitOptions::default(); if let Some(message) = message { commit_opts.set_message(message); @@ -83,12 +83,8 @@ impl Automerge { if let Some(time) = time { commit_opts.set_time(time as i64); } - let heads = self.0.commit_with(commit_opts); - let heads: Array = heads - .iter() - .map(|h| JsValue::from_str(&hex::encode(&h.0))) - .collect(); - heads + let hash = self.0.commit_with(commit_opts); + JsValue::from_str(&hex::encode(&hash.0)) } pub fn merge(&mut self, other: &mut Automerge) -> Result { @@ -585,7 +581,7 @@ impl Automerge { #[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)?; + self.0.apply_changes(changes).map_err(to_js_err)?; Ok(()) } diff --git a/automerge/examples/quickstart.rs b/automerge/examples/quickstart.rs index 1a48e1a5..db0024c6 100644 --- a/automerge/examples/quickstart.rs +++ b/automerge/examples/quickstart.rs @@ -9,7 +9,7 @@ fn main() { let mut doc1 = Automerge::new(); let (cards, card1) = doc1 .transact_with::<_, _, AutomergeError, _>( - || CommitOptions::default().with_message("Add card".to_owned()), + |_| CommitOptions::default().with_message("Add card".to_owned()), |tx| { let cards = tx.set_object(ROOT, "cards", ObjType::List).unwrap(); let card1 = tx.insert_object(&cards, 0, ObjType::Map)?; @@ -22,7 +22,7 @@ fn main() { }, ) .unwrap() - .into_result(); + .result; let mut doc2 = Automerge::new(); doc2.merge(&mut doc1).unwrap(); @@ -31,7 +31,7 @@ fn main() { let mut doc2 = Automerge::load(&binary).unwrap(); doc1.transact_with::<_, _, AutomergeError, _>( - || CommitOptions::default().with_message("Mark card as done".to_owned()), + |_| CommitOptions::default().with_message("Mark card as done".to_owned()), |tx| { tx.set(&card1, "done", true)?; Ok(()) @@ -40,7 +40,7 @@ fn main() { .unwrap(); doc2.transact_with::<_, _, AutomergeError, _>( - || CommitOptions::default().with_message("Delete card".to_owned()), + |_| CommitOptions::default().with_message("Delete card".to_owned()), |tx| { tx.del(&cards, 0)?; Ok(()) diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index 4e139765..fbcbf4a0 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -1,6 +1,5 @@ use crate::exid::ExId; use crate::transaction::{CommitOptions, Transactable}; -use crate::types::Patch; use crate::{ change::export_change, query, transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, SyncMessage, @@ -150,7 +149,7 @@ impl AutoCommit { self.doc.load_incremental(data) } - pub fn apply_changes(&mut self, changes: &[Change]) -> Result { + pub fn apply_changes(&mut self, changes: Vec) -> Result<(), AutomergeError> { self.ensure_transaction_closed(); self.doc.apply_changes(changes) } @@ -221,7 +220,7 @@ impl AutoCommit { &mut self, sync_state: &mut SyncState, message: SyncMessage, - ) -> Result, AutomergeError> { + ) -> Result<(), AutomergeError> { self.ensure_transaction_closed(); self.doc.receive_sync_message(sync_state, message) } @@ -239,13 +238,11 @@ impl AutoCommit { self.doc.get_heads() } - pub fn commit(&mut self) -> Vec { + pub fn commit(&mut self) -> ChangeHash { // ensure that even no changes triggers a change self.ensure_transaction_open(); - self.transaction - .take() - .map(|tx| tx.commit(&mut self.doc, None, None)) - .unwrap_or_else(|| self.doc.get_heads()) + let tx = self.transaction.take().unwrap(); + tx.commit(&mut self.doc, None, None) } /// Commit the current operations with some options. @@ -263,12 +260,10 @@ impl AutoCommit { /// i64; /// doc.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now)); /// ``` - pub fn commit_with(&mut self, options: CommitOptions) -> Vec { + pub fn commit_with(&mut self, options: CommitOptions) -> ChangeHash { self.ensure_transaction_open(); - self.transaction - .take() - .map(|tx| tx.commit(&mut self.doc, options.message, options.time)) - .unwrap_or_else(|| self.doc.get_heads()) + let tx = self.transaction.take().unwrap(); + tx.commit(&mut self.doc, options.message, options.time) } pub fn rollback(&mut self) -> usize { diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index b74df9dd..7fdbd9f1 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -4,12 +4,9 @@ use crate::change::encode_document; use crate::exid::ExId; use crate::keys::Keys; use crate::op_set::OpSet; -use crate::transaction::{ - CommitOptions, Transaction, TransactionFailure, TransactionInner, TransactionResult, - TransactionSuccess, -}; +use crate::transaction::{self, CommitOptions, Failure, Success, Transaction, TransactionInner}; use crate::types::{ - ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ObjId, Op, OpId, OpType, Patch, + ActorId, ChangeHash, Clock, ElemId, Export, Exportable, Key, ObjId, Op, OpId, OpType, ScalarValue, Value, }; use crate::KeysAt; @@ -114,18 +111,18 @@ impl Automerge { /// Run a transaction on this document in a closure, automatically handling commit or rollback /// afterwards. - pub fn transact(&mut self, f: F) -> TransactionResult + pub fn transact(&mut self, f: F) -> transaction::Result where F: FnOnce(&mut Transaction) -> Result, { let mut tx = self.transaction(); let result = f(&mut tx); match result { - Ok(result) => Ok(TransactionSuccess { + Ok(result) => Ok(Success { result, - heads: tx.commit(), + hash: tx.commit(), }), - Err(error) => Err(TransactionFailure { + Err(error) => Err(Failure { error, cancelled: tx.rollback(), }), @@ -133,19 +130,22 @@ impl Automerge { } /// Like [`Self::transact`] but with a function for generating the commit options. - pub fn transact_with(&mut self, c: C, f: F) -> TransactionResult + pub fn transact_with(&mut self, c: C, f: F) -> transaction::Result where F: FnOnce(&mut Transaction) -> Result, - C: FnOnce() -> CommitOptions, + C: FnOnce(&O) -> CommitOptions, { let mut tx = self.transaction(); let result = f(&mut tx); match result { - Ok(result) => Ok(TransactionSuccess { - result, - heads: tx.commit_with(c()), - }), - Err(error) => Err(TransactionFailure { + Ok(result) => { + let commit_options = c(&result); + Ok(Success { + result, + hash: tx.commit_with(commit_options), + }) + } + Err(error) => Err(Failure { error, cancelled: tx.rollback(), }), @@ -456,14 +456,14 @@ impl Automerge { pub fn load(data: &[u8]) -> Result { let changes = Change::load_document(data)?; let mut doc = Self::new(); - doc.apply_changes(&changes)?; + doc.apply_changes(changes)?; Ok(doc) } pub fn load_incremental(&mut self, data: &[u8]) -> Result { let changes = Change::load_document(data)?; let start = self.ops.len(); - self.apply_changes(&changes)?; + self.apply_changes(changes)?; let delta = self.ops.len() - start; Ok(delta) } @@ -478,26 +478,26 @@ impl Automerge { dup } - pub fn apply_changes(&mut self, changes: &[Change]) -> Result { + pub fn apply_changes(&mut self, changes: Vec) -> Result<(), AutomergeError> { for c in changes { if !self.history_index.contains_key(&c.hash) { - if self.duplicate_seq(c) { + if self.duplicate_seq(&c) { return Err(AutomergeError::DuplicateSeqNumber( c.seq, c.actor_id().clone(), )); } - if self.is_causally_ready(c) { - self.apply_change(c.clone()); + if self.is_causally_ready(&c) { + self.apply_change(c); } else { - self.queue.push(c.clone()); + self.queue.push(c); } } } while let Some(c) = self.pop_next_causally_ready_change() { self.apply_change(c); } - Ok(Patch {}) + Ok(()) } pub fn apply_change(&mut self, change: Change) { @@ -571,7 +571,7 @@ impl Automerge { .into_iter() .cloned() .collect::>(); - self.apply_changes(&changes)?; + self.apply_changes(changes)?; Ok(self.get_heads()) } diff --git a/automerge/src/sync.rs b/automerge/src/sync.rs index 862df8a5..469151fb 100644 --- a/automerge/src/sync.rs +++ b/automerge/src/sync.rs @@ -6,7 +6,6 @@ use std::{ io::Write, }; -use crate::types::Patch; use crate::{ decoding, decoding::Decoder, encoding::Encodable, Automerge, AutomergeError, Change, ChangeHash, }; @@ -98,9 +97,7 @@ impl Automerge { &mut self, sync_state: &mut SyncState, message: SyncMessage, - ) -> Result, AutomergeError> { - let mut patch = None; - + ) -> Result<(), AutomergeError> { let before_heads = self.get_heads(); let SyncMessage { @@ -112,7 +109,7 @@ impl Automerge { let changes_is_empty = message_changes.is_empty(); if !changes_is_empty { - patch = Some(self.apply_changes(&message_changes)?); + self.apply_changes(message_changes)?; sync_state.shared_heads = advance_heads( &before_heads.iter().collect(), &self.get_heads().into_iter().collect(), @@ -153,7 +150,7 @@ impl Automerge { sync_state.their_heads = Some(message_heads); sync_state.their_need = Some(message_need); - Ok(patch) + Ok(()) } fn make_bloom_filter(&self, last_sync: Vec) -> SyncHave { diff --git a/automerge/src/transaction.rs b/automerge/src/transaction.rs index fe24619e..667503ae 100644 --- a/automerge/src/transaction.rs +++ b/automerge/src/transaction.rs @@ -8,7 +8,7 @@ pub use self::commit::CommitOptions; pub use self::transactable::Transactable; pub(crate) use inner::TransactionInner; pub use manual_transaction::Transaction; -pub use result::TransactionFailure; -pub use result::TransactionSuccess; +pub use result::Failure; +pub use result::Success; -pub type TransactionResult = Result, TransactionFailure>; +pub type Result = std::result::Result, Failure>; diff --git a/automerge/src/transaction/commit.rs b/automerge/src/transaction/commit.rs index 8f7007f8..d4b12a97 100644 --- a/automerge/src/transaction/commit.rs +++ b/automerge/src/transaction/commit.rs @@ -1,8 +1,8 @@ /// Optional metadata for a commit. #[derive(Debug, Default, Clone)] pub struct CommitOptions { - pub(crate) message: Option, - pub(crate) time: Option, + pub message: Option, + pub time: Option, } impl CommitOptions { diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index f175dbb6..1d03223b 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -30,7 +30,7 @@ impl TransactionInner { doc: &mut Automerge, message: Option, time: Option, - ) -> Vec { + ) -> ChangeHash { if message.is_some() { self.message = message; } @@ -39,9 +39,11 @@ impl TransactionInner { self.time = t; } - doc.update_history(export_change(&self, &doc.ops.m.actors, &doc.ops.m.props)); - - doc.get_heads() + let change = export_change(&self, &doc.ops.m.actors, &doc.ops.m.props); + let hash = change.hash; + doc.update_history(change); + debug_assert_eq!(doc.get_heads(), vec![hash]); + hash } /// Undo the operations added in this transaction, returning the number of cancelled diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index 684df179..6a052f8c 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -32,7 +32,7 @@ impl<'a> Transaction<'a> { /// Commit the operations performed in this transaction, returning the hashes corresponding to /// the new heads. - pub fn commit(mut self) -> Vec { + pub fn commit(mut self) -> ChangeHash { self.inner.take().unwrap().commit(self.doc, None, None) } @@ -52,7 +52,7 @@ impl<'a> Transaction<'a> { /// i64; /// tx.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now)); /// ``` - pub fn commit_with(mut self, options: CommitOptions) -> Vec { + pub fn commit_with(mut self, options: CommitOptions) -> ChangeHash { self.inner .take() .unwrap() diff --git a/automerge/src/transaction/result.rs b/automerge/src/transaction/result.rs index 7f01ead2..345c9f2c 100644 --- a/automerge/src/transaction/result.rs +++ b/automerge/src/transaction/result.rs @@ -2,53 +2,18 @@ use crate::ChangeHash; /// The result of a successful, and committed, transaction. #[derive(Debug)] -pub struct TransactionSuccess { - pub(crate) result: O, - pub(crate) heads: Vec, -} - -impl TransactionSuccess { - /// Get the result of the transaction. - pub fn result(&self) -> &O { - &self.result - } - - /// Get the result of the transaction. - pub fn into_result(self) -> O { - self.result - } - - /// Get the new heads of the document after commiting the transaction. - pub fn heads(&self) -> &[ChangeHash] { - &self.heads - } - - /// Get the new heads of the document after commiting the transaction. - pub fn into_heads(self) -> Vec { - self.heads - } +pub struct Success { + /// The result of the transaction. + pub result: O, + /// The hash of the change, also the head of the document. + pub hash: ChangeHash, } /// The result of a failed, and rolled back, transaction. #[derive(Debug)] -pub struct TransactionFailure { - pub(crate) error: E, - pub(crate) cancelled: usize, -} - -impl TransactionFailure { - /// Get the error of the transaction. - pub fn error(&self) -> &E { - &self.error - } - - /// Get the error of the transaction. - pub fn into_error(self) -> E { - self.error - } - - /// Get the number of cancelled operations in the transaction. - pub fn cancelled(&self) -> usize { - self.cancelled - } +pub struct Failure { + /// The error returned from the transaction. + pub error: E, + /// The number of operations cancelled. + pub cancelled: usize, } diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 684324c9..6fa21e73 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -336,9 +336,6 @@ pub enum Prop { Seq(usize), } -#[derive(Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] -pub struct Patch {} - impl Key { pub fn elemid(&self) -> Option { match self { From b14d874dfc074f499598ea54af570effa167df5b Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 12:43:52 +0000 Subject: [PATCH 32/45] Move sync structs to module --- automerge-wasm/src/interop.rs | 22 ++++++++++----------- automerge-wasm/src/lib.rs | 8 ++++---- automerge-wasm/src/sync.rs | 4 ++-- automerge/src/autocommit.rs | 11 +++++------ automerge/src/lib.rs | 3 +-- automerge/src/sync.rs | 36 +++++++++++++++++------------------ automerge/src/sync/state.rs | 12 ++++++------ 7 files changed, 47 insertions(+), 49 deletions(-) diff --git a/automerge-wasm/src/interop.rs b/automerge-wasm/src/interop.rs index 5f35959c..104f5f1d 100644 --- a/automerge-wasm/src/interop.rs +++ b/automerge-wasm/src/interop.rs @@ -25,8 +25,8 @@ impl From for JsValue { } } -impl From for JS { - fn from(state: am::SyncState) -> Self { +impl From for JS { + fn from(state: am::sync::State) -> Self { let shared_heads: JS = state.shared_heads.into(); let last_sent_heads: JS = state.last_sent_heads.into(); let their_heads: JS = state.their_heads.into(); @@ -133,7 +133,7 @@ impl TryFrom for Vec { } } -impl TryFrom for am::SyncState { +impl TryFrom for am::sync::State { type Error = JsValue; fn try_from(value: JS) -> Result { @@ -144,7 +144,7 @@ impl TryFrom for am::SyncState { let their_need = js_get(&value, "theirNeed")?.into(); let their_have = js_get(&value, "theirHave")?.try_into()?; let sent_hashes = js_get(&value, "sentHashes")?.try_into()?; - Ok(am::SyncState { + Ok(am::sync::State { shared_heads, last_sent_heads, their_heads, @@ -155,7 +155,7 @@ impl TryFrom for am::SyncState { } } -impl TryFrom for Option> { +impl TryFrom for Option> { type Error = JsValue; fn try_from(value: JS) -> Result { @@ -167,17 +167,17 @@ impl TryFrom for Option> { } } -impl TryFrom for Vec { +impl TryFrom for Vec { type Error = JsValue; fn try_from(value: JS) -> Result { let value = value.0.dyn_into::()?; - let have: Result, JsValue> = value + let have: Result, JsValue> = value .iter() .map(|s| { let last_sync = js_get(&s, "lastSync")?.try_into()?; let bloom = js_get(&s, "bloom")?.try_into()?; - Ok(am::SyncHave { last_sync, bloom }) + Ok(am::sync::Have { last_sync, bloom }) }) .collect(); let have = have?; @@ -185,7 +185,7 @@ impl TryFrom for Vec { } } -impl TryFrom for am::BloomFilter { +impl TryFrom for am::sync::BloomFilter { type Error = JsValue; fn try_from(value: JS) -> Result { @@ -215,8 +215,8 @@ impl From<&[Change]> for AR { } } -impl From<&[am::SyncHave]> for AR { - fn from(value: &[am::SyncHave]) -> Self { +impl From<&[am::sync::Have]> for AR { + fn from(value: &[am::sync::Have]) -> Self { AR(value .iter() .map(|have| { diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 04da499a..b8240021 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -653,7 +653,7 @@ impl Automerge { message: Uint8Array, ) -> Result<(), JsValue> { let message = message.to_vec(); - let message = am::SyncMessage::decode(message.as_slice()).map_err(to_js_err)?; + let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?; self.0 .receive_sync_message(&mut state.0, message) .map_err(to_js_err)?; @@ -785,7 +785,7 @@ pub fn decode_change(change: Uint8Array) -> Result { #[wasm_bindgen(js_name = initSyncState)] pub fn init_sync_state() -> SyncState { - SyncState(am::SyncState::new()) + SyncState(am::sync::State::new()) } // this is needed to be compatible with the automerge-js api @@ -807,7 +807,7 @@ pub fn encode_sync_message(message: JsValue) -> Result { let changes = js_get(&message, "changes")?.try_into()?; let have = js_get(&message, "have")?.try_into()?; Ok(Uint8Array::from( - am::SyncMessage { + am::sync::Message { heads, need, have, @@ -821,7 +821,7 @@ pub fn encode_sync_message(message: JsValue) -> Result { #[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 msg = am::sync::Message::decode(&data).map_err(to_js_err)?; let heads = AR::from(msg.heads.as_slice()); let need = AR::from(msg.need.as_slice()); let changes = AR::from(msg.changes.as_slice()); diff --git a/automerge-wasm/src/sync.rs b/automerge-wasm/src/sync.rs index 7c201e61..5a24a28c 100644 --- a/automerge-wasm/src/sync.rs +++ b/automerge-wasm/src/sync.rs @@ -9,7 +9,7 @@ use crate::interop::{to_js_err, AR, JS}; #[wasm_bindgen] #[derive(Debug)] -pub struct SyncState(pub(crate) am::SyncState); +pub struct SyncState(pub(crate) am::sync::State); #[wasm_bindgen] impl SyncState { @@ -45,7 +45,7 @@ impl SyncState { pub(crate) fn decode(data: Uint8Array) -> Result { let data = data.to_vec(); - let s = am::SyncState::decode(&data); + let s = am::sync::State::decode(&data); let s = s.map_err(to_js_err)?; Ok(SyncState(s)) } diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index fbcbf4a0..de8842b3 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::{ - change::export_change, query, transaction::TransactionInner, ActorId, Automerge, - AutomergeError, Change, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, SyncMessage, - SyncState, Value, + change::export_change, query, sync, transaction::TransactionInner, ActorId, Automerge, + AutomergeError, Change, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value, }; /// An automerge document that automatically manages transactions. @@ -211,15 +210,15 @@ impl AutoCommit { self.doc.dump() } - pub fn generate_sync_message(&mut self, sync_state: &mut SyncState) -> Option { + pub fn generate_sync_message(&mut self, sync_state: &mut sync::State) -> Option { self.ensure_transaction_closed(); self.doc.generate_sync_message(sync_state) } pub fn receive_sync_message( &mut self, - sync_state: &mut SyncState, - message: SyncMessage, + sync_state: &mut sync::State, + message: sync::Message, ) -> Result<(), AutomergeError> { self.ensure_transaction_closed(); self.doc.receive_sync_message(sync_state, message) diff --git a/automerge/src/lib.rs b/automerge/src/lib.rs index f3c62dce..a98ef14e 100644 --- a/automerge/src/lib.rs +++ b/automerge/src/lib.rs @@ -40,7 +40,7 @@ mod legacy; mod op_set; mod op_tree; mod query; -mod sync; +pub mod sync; pub mod transaction; mod types; mod value; @@ -55,7 +55,6 @@ pub use exid::ExId as ObjId; pub use keys::Keys; pub use keys_at::KeysAt; pub use legacy::Change as ExpandedChange; -pub use sync::{BloomFilter, SyncHave, SyncMessage, SyncState}; pub use types::{ActorId, ChangeHash, ObjType, OpType, Prop}; pub use value::{ScalarValue, Value}; diff --git a/automerge/src/sync.rs b/automerge/src/sync.rs index 469151fb..cc4aca51 100644 --- a/automerge/src/sync.rs +++ b/automerge/src/sync.rs @@ -14,13 +14,13 @@ mod bloom; mod state; pub use bloom::BloomFilter; -pub use state::{SyncHave, SyncState}; +pub use state::{Have, State}; const HASH_SIZE: usize = 32; // 256 bits = 32 bytes const MESSAGE_TYPE_SYNC: u8 = 0x42; // first byte of a sync message, for identification impl Automerge { - pub fn generate_sync_message(&self, sync_state: &mut SyncState) -> Option { + pub fn generate_sync_message(&self, sync_state: &mut State) -> Option { let our_heads = self.get_heads(); let our_need = self.get_missing_deps(sync_state.their_heads.as_ref().unwrap_or(&vec![])); @@ -43,10 +43,10 @@ impl Automerge { .iter() .all(|hash| self.get_change_by_hash(hash).is_some()) { - let reset_msg = SyncMessage { + let reset_msg = Message { heads: our_heads, need: Vec::new(), - have: vec![SyncHave::default()], + have: vec![Have::default()], changes: Vec::new(), }; return Some(reset_msg); @@ -83,7 +83,7 @@ impl Automerge { .sent_hashes .extend(changes_to_send.iter().map(|c| c.hash)); - let sync_message = SyncMessage { + let sync_message = Message { heads: our_heads, have: our_have, need: our_need, @@ -95,12 +95,12 @@ impl Automerge { pub fn receive_sync_message( &mut self, - sync_state: &mut SyncState, - message: SyncMessage, + sync_state: &mut State, + message: Message, ) -> Result<(), AutomergeError> { let before_heads = self.get_heads(); - let SyncMessage { + let Message { heads: message_heads, changes: message_changes, need: message_need, @@ -153,19 +153,19 @@ impl Automerge { Ok(()) } - fn make_bloom_filter(&self, last_sync: Vec) -> SyncHave { + fn make_bloom_filter(&self, last_sync: Vec) -> Have { let new_changes = self.get_changes(&last_sync); let hashes = new_changes .into_iter() .map(|change| change.hash) .collect::>(); - SyncHave { + Have { last_sync, bloom: BloomFilter::from(&hashes[..]), } } - fn get_changes_to_send(&self, have: Vec, need: &[ChangeHash]) -> Vec<&Change> { + fn get_changes_to_send(&self, have: Vec, need: &[ChangeHash]) -> Vec<&Change> { if have.is_empty() { need.iter() .filter_map(|hash| self.get_change_by_hash(hash)) @@ -175,7 +175,7 @@ impl Automerge { let mut bloom_filters = Vec::with_capacity(have.len()); for h in have { - let SyncHave { last_sync, bloom } = h; + let Have { last_sync, bloom } = h; for hash in last_sync { last_sync_hashes.insert(hash); } @@ -237,14 +237,14 @@ impl Automerge { } #[derive(Debug, Clone)] -pub struct SyncMessage { +pub struct Message { pub heads: Vec, pub need: Vec, - pub have: Vec, + pub have: Vec, pub changes: Vec, } -impl SyncMessage { +impl Message { pub fn encode(self) -> Vec { let mut buf = vec![MESSAGE_TYPE_SYNC]; @@ -265,7 +265,7 @@ impl SyncMessage { buf } - pub fn decode(bytes: &[u8]) -> Result { + pub fn decode(bytes: &[u8]) -> Result { let mut decoder = Decoder::new(Cow::Borrowed(bytes)); let message_type = decoder.read::()?; @@ -284,7 +284,7 @@ impl SyncMessage { let last_sync = decode_hashes(&mut decoder)?; let bloom_bytes: Vec = decoder.read()?; let bloom = BloomFilter::try_from(bloom_bytes.as_slice())?; - have.push(SyncHave { last_sync, bloom }); + have.push(Have { last_sync, bloom }); } let change_count = decoder.read::()?; @@ -294,7 +294,7 @@ impl SyncMessage { changes.push(Change::from_bytes(change)?); } - Ok(SyncMessage { + Ok(Message { heads, need, have, diff --git a/automerge/src/sync/state.rs b/automerge/src/sync/state.rs index 48b183b4..bc1360e2 100644 --- a/automerge/src/sync/state.rs +++ b/automerge/src/sync/state.rs @@ -1,27 +1,27 @@ use std::{borrow::Cow, collections::HashSet}; -use super::{decode_hashes, encode_hashes}; -use crate::{decoding, decoding::Decoder, BloomFilter, ChangeHash}; +use super::{decode_hashes, encode_hashes, BloomFilter}; +use crate::{decoding, decoding::Decoder, ChangeHash}; const SYNC_STATE_TYPE: u8 = 0x43; // first byte of an encoded sync state, for identification #[derive(Debug, Clone, Default)] -pub struct SyncState { +pub struct State { pub shared_heads: Vec, pub last_sent_heads: Vec, pub their_heads: Option>, pub their_need: Option>, - pub their_have: Option>, + pub their_have: Option>, pub sent_hashes: HashSet, } #[derive(Debug, Clone, Default)] -pub struct SyncHave { +pub struct Have { pub last_sync: Vec, pub bloom: BloomFilter, } -impl SyncState { +impl State { pub fn new() -> Self { Default::default() } From ff1a20c626a6f45ffb4261d9749a1fc7a888c52e Mon Sep 17 00:00:00 2001 From: Andrew Jeffery Date: Wed, 9 Mar 2022 13:04:10 +0000 Subject: [PATCH 33/45] Document some sync api --- automerge/src/sync.rs | 5 +++++ automerge/src/sync/state.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/automerge/src/sync.rs b/automerge/src/sync.rs index cc4aca51..43801b9c 100644 --- a/automerge/src/sync.rs +++ b/automerge/src/sync.rs @@ -236,11 +236,16 @@ impl Automerge { } } +/// The sync message to be sent. #[derive(Debug, Clone)] pub struct Message { + /// The heads of the sender. pub heads: Vec, + /// The hashes of any changes that are being explicitly requested from the recipient. pub need: Vec, + /// A summary of the changes that the sender already has. pub have: Vec, + /// The changes for the recipient to apply. pub changes: Vec, } diff --git a/automerge/src/sync/state.rs b/automerge/src/sync/state.rs index bc1360e2..9828060c 100644 --- a/automerge/src/sync/state.rs +++ b/automerge/src/sync/state.rs @@ -5,6 +5,7 @@ use crate::{decoding, decoding::Decoder, ChangeHash}; const SYNC_STATE_TYPE: u8 = 0x43; // first byte of an encoded sync state, for identification +/// The state of synchronisation with a peer. #[derive(Debug, Clone, Default)] pub struct State { pub shared_heads: Vec, @@ -15,9 +16,15 @@ pub struct State { pub sent_hashes: HashSet, } +/// A summary of the changes that the sender of the message already has. +/// This is implicitly a request to the recipient to send all changes that the +/// sender does not already have. #[derive(Debug, Clone, Default)] pub struct Have { + /// The heads at the time of the last successful sync with this recipient. pub last_sync: Vec, + /// A bloom filter summarising all of the changes that the sender of the message has added + /// since the last sync. pub bloom: BloomFilter, } From 57a0f62b75053a8eb3f595b6ff1350e8460054dd Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 10 Mar 2022 09:23:23 -0500 Subject: [PATCH 34/45] v0.0.19 wasm --- automerge-wasm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 21ea5b0d..2cabdf18 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.18", + "version": "0.0.19", "license": "MIT", "files": [ "README.md", From e00797c512f16ce78ea5d9d49c920f11c6017c95 Mon Sep 17 00:00:00 2001 From: Blaine Cook Date: Thu, 10 Mar 2022 15:42:57 -0800 Subject: [PATCH 35/45] test for attribution correctly not surfacing temporary text (inserted and deleted after baseline) --- automerge-wasm/test/attribute.ts | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/automerge-wasm/test/attribute.ts b/automerge-wasm/test/attribute.ts index 50aff286..97cbb0d0 100644 --- a/automerge-wasm/test/attribute.ts +++ b/automerge-wasm/test/attribute.ts @@ -79,6 +79,37 @@ describe('Automerge', () => { { 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', () => { @@ -109,5 +140,35 @@ describe('Automerge', () => { { 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: [] } + ]) + }) }) }) From ae87d7bc007b45f77780a35e4af9ead4119192c0 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 14 Mar 2022 14:47:12 -0400 Subject: [PATCH 36/45] v20 - object replacement char --- automerge-wasm/package.json | 2 +- automerge-wasm/test/test.ts | 9 +++++++++ automerge/src/automerge.rs | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 2cabdf18..a71612f5 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.19", + "version": "0.0.20", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index ac3f4413..e4e3420d 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -240,6 +240,15 @@ describe('Automerge', () => { doc.free() }) + it('should be able to insert objects into text', () => { + let doc = create() + let text = doc.set_object("/", "text", "Hello world"); + let obj = doc.insert_object(text, 6, { hello: "world" }); + assert.deepEqual(doc.text(text), "Hello \ufffcworld"); + assert.deepEqual(doc.value(text, 6), ["map", obj]); + assert.deepEqual(doc.value(obj, "hello"), ["str", "world"]); + }) + it('should be able save all or incrementally', () => { let doc = create() diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index ee9d6a75..eab9a31e 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -274,6 +274,8 @@ impl Automerge { for q in &query.ops { if let OpType::Set(ScalarValue::Str(s)) = &q.action { buffer.push_str(s); + } else { + buffer.push('\u{fffc}'); } } Ok(buffer) From 0cf54c36a8ef244be32753eaeafb2d95a853999f Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Thu, 17 Mar 2022 19:15:21 -0400 Subject: [PATCH 37/45] 0.0.21 --- automerge-js/package.json | 2 +- automerge-wasm/package.json | 2 +- automerge-wasm/test/marks.ts | 36 +++++++++++++------------------- automerge/src/automerge.rs | 13 ++++++------ automerge/src/query.rs | 1 - automerge/src/query/raw_spans.rs | 2 -- 6 files changed, 22 insertions(+), 34 deletions(-) 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/package.json b/automerge-wasm/package.json index a71612f5..48be6956 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.20", + "version": "0.0.21", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index 76702caf..a016a7a2 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -8,10 +8,9 @@ describe('Automerge', () => { describe('marks', () => { it('should handle marks [..]', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') - doc.splice(list, 0, 0, "aaabbbccc") - doc.mark(list, "[3..6]", "bold" , true) + 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") @@ -22,8 +21,7 @@ describe('Automerge', () => { it('should handle marks [..] at the beginning of a string', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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); @@ -39,8 +37,7 @@ describe('Automerge', () => { it('should handle marks [..] with splice', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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); @@ -56,8 +53,7 @@ describe('Automerge', () => { it('should handle marks across multiple forks', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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); @@ -79,8 +75,7 @@ describe('Automerge', () => { it('should handle marks with deleted ends [..]', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + let list = doc.set_object("_root", "list", "") doc.splice(list, 0, 0, "aaabbbccc") doc.mark(list, "[3..6]", "bold" , true) @@ -100,8 +95,7 @@ describe('Automerge', () => { it('should handle sticky marks (..)', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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); @@ -114,8 +108,7 @@ describe('Automerge', () => { it('should handle sticky marks with deleted ends (..)', () => { let doc = create() - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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); @@ -143,13 +136,12 @@ describe('Automerge', () => { it('should handle overlapping marks', () => { let doc : Automerge = create("aabbcc") - let list = doc.make("_root", "list", "") - if (!list) throw new Error('should not be undefined') + 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",999); + doc.commit("marks"); let spans = doc.spans(list); assert.deepStrictEqual(spans, [ @@ -175,9 +167,9 @@ describe('Automerge', () => { let raw_spans = doc.raw_spans(list); assert.deepStrictEqual(raw_spans, [ - { id: "39@aabbcc", time: 999, start: 0, end: 37, type: 'bold', value: true }, - { id: "41@aabbcc", time: 999, start: 4, end: 19, type: 'itallic', value: true }, - { id: "43@aabbcc", time: 999, start: 10, end: 13, type: 'comment', value: 'foxes are my favorite animal!' } + { 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!' } ]); // mark sure encode decode can handle marks diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 68426066..7278dae8 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -301,7 +301,7 @@ impl Automerge { 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()); + let query = self.ops.search(&obj, query::ListVals::new()); Ok(query .ops .iter() @@ -316,7 +316,7 @@ impl Automerge { ) -> 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)); + let query = self.ops.search(&obj, query::ListValsAt::new(clock)); Ok(query .ops .iter() @@ -326,7 +326,7 @@ impl Automerge { 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()); + let mut query = self.ops.search(&obj, query::Spans::new()); query.check_marks(); Ok(query.spans) } @@ -342,7 +342,7 @@ impl Automerge { 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)); + .search(&obj, query::Attribute::new(baseline, change_sets)); query.finish(); Ok(query.change_sets) } @@ -358,7 +358,7 @@ impl Automerge { 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)); + .search(&obj, query::Attribute2::new(baseline, change_sets)); query.finish(); Ok(query.change_sets) } @@ -368,13 +368,12 @@ impl Automerge { obj: O, ) -> Result, AutomergeError> { let obj = self.exid_to_obj(obj.as_ref())?; - let query = self.ops.search(obj, query::RawSpans::new()); + 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), - time: self.history[s.change].time, start: s.start, end: s.end, span_type: s.name, diff --git a/automerge/src/query.rs b/automerge/src/query.rs index c34af84b..b44a6534 100644 --- a/automerge/src/query.rs +++ b/automerge/src/query.rs @@ -46,7 +46,6 @@ pub(crate) use spans::{Span, Spans}; #[derive(Serialize, Debug, Clone, PartialEq)] pub struct SpanInfo { pub id: ExId, - pub time: i64, pub start: usize, pub end: usize, #[serde(rename = "type")] diff --git a/automerge/src/query/raw_spans.rs b/automerge/src/query/raw_spans.rs index 95aafc56..e375e683 100644 --- a/automerge/src/query/raw_spans.rs +++ b/automerge/src/query/raw_spans.rs @@ -15,7 +15,6 @@ pub(crate) struct RawSpans { #[derive(Debug, Clone, PartialEq)] pub(crate) struct RawSpan { pub id: OpId, - pub change: usize, pub start: usize, pub end: usize, pub name: String, @@ -49,7 +48,6 @@ impl TreeQuery for RawSpans { pos, RawSpan { id: element.id, - change: element.change, start: self.seen, end: 0, name: md.name.clone(), From 25afa0b12bbba1776da1333be3acc0f50bcfd7ff Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 21 Mar 2022 13:36:01 -0400 Subject: [PATCH 38/45] unmark() - 0.0.21 --- automerge-wasm/index.d.ts | 1 + automerge-wasm/package.json | 2 +- automerge-wasm/src/lib.rs | 7 +++ automerge-wasm/test/marks.ts | 16 +++++++ automerge/src/autocommit.rs | 8 +++- automerge/src/transaction/inner.rs | 46 +++++++++++++++++-- .../src/transaction/manual_transaction.rs | 6 ++- automerge/src/transaction/transactable.rs | 2 + automerge/src/types.rs | 4 ++ 9 files changed, 86 insertions(+), 6 deletions(-) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index ccb2a50a..0f8ef99c 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -121,6 +121,7 @@ export class Automerge { // 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[]; diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index 48be6956..b34ce583 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.21", + "version": "0.0.22", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 64165d16..87a789a9 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -397,6 +397,13 @@ impl Automerge { 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)?; diff --git a/automerge-wasm/test/marks.ts b/automerge-wasm/test/marks.ts index a016a7a2..73146979 100644 --- a/automerge-wasm/test/marks.ts +++ b/automerge-wasm/test/marks.ts @@ -172,14 +172,30 @@ describe('Automerge', () => { { 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/src/autocommit.rs b/automerge/src/autocommit.rs index 449815e1..44edb4a6 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -367,7 +367,7 @@ impl Transactable for AutoCommit { let tx = self.transaction.as_mut().unwrap(); tx.mark( &mut self.doc, - obj.as_ref(), + obj, start, expand_start, end, @@ -377,6 +377,12 @@ impl Transactable for AutoCommit { ) } + 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, diff --git a/automerge/src/transaction/inner.rs b/automerge/src/transaction/inner.rs index fe36176c..436f1519 100644 --- a/automerge/src/transaction/inner.rs +++ b/automerge/src/transaction/inner.rs @@ -148,10 +148,10 @@ impl TransactionInner { } #[allow(clippy::too_many_arguments)] - pub fn mark( + pub fn mark>( &mut self, doc: &mut Automerge, - obj: &ExId, + obj: O, start: usize, expand_start: bool, end: usize, @@ -159,7 +159,7 @@ impl TransactionInner { mark: &str, value: ScalarValue, ) -> Result<(), AutomergeError> { - let obj = doc.exid_to_obj(obj)?; + let obj = doc.exid_to_obj(obj.as_ref())?; self.do_insert( doc, @@ -172,6 +172,46 @@ impl TransactionInner { 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, diff --git a/automerge/src/transaction/manual_transaction.rs b/automerge/src/transaction/manual_transaction.rs index 3cc72923..cdfd7530 100644 --- a/automerge/src/transaction/manual_transaction.rs +++ b/automerge/src/transaction/manual_transaction.rs @@ -134,7 +134,7 @@ impl<'a> Transactable for Transaction<'a> { ) -> Result<(), AutomergeError> { self.inner.as_mut().unwrap().mark( self.doc, - obj.as_ref(), + obj, start, expand_start, end, @@ -144,6 +144,10 @@ impl<'a> Transactable for Transaction<'a> { ) } + 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, diff --git a/automerge/src/transaction/transactable.rs b/automerge/src/transaction/transactable.rs index 882ee8a6..fa8e7ac9 100644 --- a/automerge/src/transaction/transactable.rs +++ b/automerge/src/transaction/transactable.rs @@ -71,6 +71,8 @@ pub trait Transactable { 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, diff --git a/automerge/src/types.rs b/automerge/src/types.rs index 620c9030..87e1c887 100644 --- a/automerge/src/types.rs +++ b/automerge/src/types.rs @@ -211,6 +211,10 @@ impl OpId { 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 { From 5d4e1f0c4266bfdcfd1c1471e9d7718ff0c083f8 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Mon, 21 Mar 2022 17:36:11 -0400 Subject: [PATCH 39/45] return touched objects from apply_changes --- automerge-wasm/index.d.ts | 8 ++++---- automerge-wasm/src/lib.rs | 31 ++++++++++++++++--------------- automerge-wasm/test/test.ts | 21 ++++++++++++++------- automerge/src/autocommit.rs | 8 ++++---- automerge/src/automerge.rs | 30 ++++++++++++++++-------------- automerge/src/op_set.rs | 4 ---- automerge/src/sync.rs | 15 ++++++++------- 7 files changed, 62 insertions(+), 55 deletions(-) diff --git a/automerge-wasm/index.d.ts b/automerge-wasm/index.d.ts index 2897955e..1e3351ab 100644 --- a/automerge-wasm/index.d.ts +++ b/automerge-wasm/index.d.ts @@ -104,7 +104,7 @@ export class Automerge { // transactions commit(message?: string, time?: number): Heads; - merge(other: Automerge): Heads; + merge(other: Automerge): ObjID[]; getActorId(): Actor; pendingOps(): number; rollback(): number; @@ -112,14 +112,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[]; getChangesAdded(other: Automerge): Change[]; getHeads(): Heads; diff --git a/automerge-wasm/src/lib.rs b/automerge-wasm/src/lib.rs index 63b7e78a..a5d4e978 100644 --- a/automerge-wasm/src/lib.rs +++ b/automerge-wasm/src/lib.rs @@ -89,12 +89,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 { @@ -365,17 +362,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)] @@ -444,13 +443,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/test.ts b/automerge-wasm/test/test.ts index 4c9b08be..fc3dd368 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -276,8 +276,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 +345,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'], @@ -1073,16 +1076,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 75b5dbe4..5371a4f9 100644 --- a/automerge/src/autocommit.rs +++ b/automerge/src/autocommit.rs @@ -139,18 +139,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) @@ -210,7 +210,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) } diff --git a/automerge/src/automerge.rs b/automerge/src/automerge.rs index 8316f088..291f24e6 100644 --- a/automerge/src/automerge.rs +++ b/automerge/src/automerge.rs @@ -264,7 +264,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. @@ -404,12 +408,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 { @@ -423,7 +424,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) { @@ -433,23 +435,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); } } @@ -511,15 +514,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. diff --git a/automerge/src/op_set.rs b/automerge/src/op_set.rs index bd84911d..315d221b 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/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 { From bc98b1ecc9a812f5e945e35f51d258374a9bd86b Mon Sep 17 00:00:00 2001 From: Rae Mac <633012+okdistribute@users.noreply.github.com> Date: Tue, 22 Mar 2022 10:06:13 -0700 Subject: [PATCH 40/45] Example test for confusing behavior --- automerge-wasm/test/test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index b272fcf4..159c9019 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -462,6 +462,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, 1) + }) }) describe('sync', () => { it('should send a sync message implying no local data', () => { From 2663e0315cfd3ef063e21e250fd1fec49a8aa8a9 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 22 Mar 2022 13:38:46 -0400 Subject: [PATCH 41/45] fix test --- automerge-wasm/test/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index 159c9019..8a9b657c 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -473,7 +473,7 @@ describe('Automerge', () => { A.insert('/list', 2, 'c') let opIds = A.merge(B) - assert.equal(opIds.length, 1) + assert.equal(opIds.length, 0) }) }) describe('sync', () => { From af02ba6b861c12c1a0a99e7bf045028109a352fb Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Wed, 23 Mar 2022 09:49:06 -0400 Subject: [PATCH 42/45] 0.0.23 - getChangeByHash --- automerge-wasm/package.json | 2 +- automerge-wasm/test/test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/automerge-wasm/package.json b/automerge-wasm/package.json index b34ce583..336f78f6 100644 --- a/automerge-wasm/package.json +++ b/automerge-wasm/package.json @@ -6,7 +6,7 @@ ], "name": "automerge-wasm-pack", "description": "wasm-bindgen bindings to the automerge rust implementation", - "version": "0.0.22", + "version": "0.0.23", "license": "MIT", "files": [ "README.md", diff --git a/automerge-wasm/test/test.ts b/automerge-wasm/test/test.ts index aea16db2..97c667b6 100644 --- a/automerge-wasm/test/test.ts +++ b/automerge-wasm/test/test.ts @@ -389,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', () => { From c149da3a6df69b30ba0f28321da02ac130ec75e4 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Sat, 26 Mar 2022 13:31:39 -0400 Subject: [PATCH 43/45] attr bug --- automerge-wasm/attr_bug.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 automerge-wasm/attr_bug.js 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)) + From 08e6a86f282e4534025b2af8f6c87b7fa74c9762 Mon Sep 17 00:00:00 2001 From: Orion Henry Date: Tue, 29 Mar 2022 12:07:59 -0400 Subject: [PATCH 44/45] fmt --- automerge/src/autocommit.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/automerge/src/autocommit.rs b/automerge/src/autocommit.rs index a50df9b5..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::{ - change::export_change, query, sync, transaction::TransactionInner, ActorId, Automerge, - AutomergeError, Change, ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value, + query, sync, transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, + ChangeHash, Keys, KeysAt, ObjType, Prop, ScalarValue, Value, }; /// An automerge document that automatically manages transactions. From 4e304d11c6fdf0c3402da0d76cbf0f9f856c2ad7 Mon Sep 17 00:00:00 2001 From: Rae Mac <633012+okdistribute@users.noreply.github.com> Date: Tue, 29 Mar 2022 16:06:08 -0700 Subject: [PATCH 45/45] attribute deletion test --- automerge-wasm/test/attribute.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/automerge-wasm/test/attribute.ts b/automerge-wasm/test/attribute.ts index 97cbb0d0..a83dc5d4 100644 --- a/automerge-wasm/test/attribute.ts +++ b/automerge-wasm/test/attribute.ts @@ -169,6 +169,21 @@ describe('Automerge', () => { { 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: [] } + ]) }) }) })