bugfix: duplicate seq not blocked on apply_changes, clone did not close a transaction, added fork and merge to wasm

This commit is contained in:
Orion Henry 2022-02-10 11:14:44 -05:00
parent ec3f6cd69f
commit 80640ac799
6 changed files with 90 additions and 42 deletions

View file

@ -89,7 +89,7 @@ export class Automerge {
make(obj: ObjID, prop: Prop, value: ObjectType): 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<Value | FullValue>): ObjID[] | undefined;
splice(obj: ObjID, start: number, delete_count: number, text?: string | Array<Value | FullValue>): ObjID[] | undefined;
inc(obj: ObjID, prop: Prop, value: number): void;
del(obj: ObjID, prop: Prop): void;
@ -108,6 +108,7 @@ export class Automerge {
// transactions
commit(message?: string, time?: number): Heads;
merge(other: Automerge): Heads;
getActorId(): Actor;
pendingOps(): number;
rollback(): number;
@ -132,6 +133,7 @@ export class Automerge {
// memory management
free(): void;
clone(actor?: string): Automerge;
fork(actor?: string): Automerge;
// dump internal state to console.log
dump(): void;

View file

@ -44,7 +44,10 @@ impl Automerge {
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&self, actor: Option<String>) -> Result<Automerge, JsValue> {
pub fn clone(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
if self.0.pending_ops() > 0 {
self.0.commit(None,None);
}
let mut automerge = Automerge(self.0.clone());
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
@ -53,6 +56,16 @@ impl Automerge {
Ok(automerge)
}
#[allow(clippy::should_implement_trait)]
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
let mut automerge = Automerge(self.0.fork());
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.0.set_actor(actor)
}
Ok(automerge)
}
pub fn free(self) {}
#[wasm_bindgen(js_name = pendingOps)]
@ -69,6 +82,15 @@ impl Automerge {
heads
}
pub fn merge(&mut self, other: &mut Automerge) -> Result<Array,JsError> {
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)
}
pub fn rollback(&mut self) -> f64 {
self.0.rollback() as f64
}
@ -89,11 +111,10 @@ impl Automerge {
pub fn text(&mut self, obj: String, heads: Option<Array>) -> Result<String, JsValue> {
let obj = self.import(obj)?;
if let Some(heads) = get_heads(heads) {
self.0.text_at(&obj, &heads)
Ok(self.0.text_at(&obj, &heads)?)
} else {
self.0.text(&obj)
Ok(self.0.text(&obj)?)
}
.map_err(to_js_err)
}
pub fn splice(
@ -109,8 +130,7 @@ impl Automerge {
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)?;
.splice_text(&obj, start, delete_count, &t)?;
Ok(None)
} else {
if let Ok(array) = text.dyn_into::<Array>() {
@ -128,8 +148,7 @@ impl Automerge {
}
let result = self
.0
.splice(&obj, start, delete_count, vals)
.map_err(to_js_err)?;
.splice(&obj, start, delete_count, vals)?;
if result.is_empty() {
Ok(None)
} else {
@ -151,7 +170,7 @@ impl Automerge {
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)?;
let opid = self.0.insert(&obj, index, value)?;
Ok(opid.map(|id| id.to_string()))
}
@ -167,8 +186,7 @@ impl Automerge {
let value = self.import_value(value, datatype)?;
let opid = self
.0
.insert(&obj, index as usize, value)
.map_err(to_js_err)?;
.insert(&obj, index as usize, value)?;
Ok(opid.map(|id| id.to_string()))
}
@ -182,7 +200,7 @@ impl Automerge {
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)?;
let opid = self.0.set(&obj, prop, value)?;
Ok(opid.map(|id| id.to_string()))
}
@ -191,7 +209,7 @@ impl Automerge {
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)?;
let opid = self.0.set(&obj, prop, value)?;
Ok(opid.unwrap().to_string())
} else {
Err(to_js_err("invalid object type"))
@ -203,9 +221,8 @@ impl Automerge {
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_or(to_js_err("inc needs a numberic value"))?;
self.0.inc(&obj, prop, value as i64)?;
Ok(())
}
@ -221,11 +238,10 @@ impl Automerge {
let heads = get_heads(heads);
if let Ok(prop) = prop {
let value = if let Some(h) = heads {
self.0.value_at(&obj, prop, &h)
self.0.value_at(&obj, prop, &h)?
} else {
self.0.value(&obj, prop)
}
.map_err(to_js_err)?;
self.0.value(&obj, prop)?
};
match value {
Some((Value::Object(obj_type), obj_id)) => {
result.push(&obj_type.to_string().into());
@ -408,8 +424,8 @@ impl Automerge {
}
#[wasm_bindgen(js_name = getChangesAdded)]
pub fn get_changes_added(&mut self, other: &Automerge) -> Result<Array, JsValue> {
let changes = self.0.get_changes_added(&other.0);
pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
let changes = self.0.get_changes_added(&mut other.0);
let changes: Array = changes
.iter()
.map(|c| Uint8Array::from(c.raw_bytes()))

View file

@ -539,28 +539,27 @@ describe('Automerge', () => {
})
it('should handle merging text conflicts then saving & loading', () => {
let A = create()
let A = create("aabbcc")
let At = A.make('_root', 'text', TEXT)
A.splice(At, 0, 0, Array.from('hello'))
A.splice(At, 0, 0, 'hello')
let B = A.clone()
let Bt = B.value('_root', 'text')
if (!Bt || Bt[0] !== 'text') return assert.fail()
let obj = Bt[1]
B.splice(obj, 4, 1, '')
B.splice(obj, 4, 0, '!')
B.splice(obj, 5, 0, ' ')
B.splice(obj, 6, 0, Array.from('world'))
let B = A.fork()
A.applyChanges(B.getChanges(A.getHeads()))
assert.deepEqual(B.value("_root","text"), [ "text", At])
B.splice(At, 4, 1)
B.splice(At, 4, 0, '!')
B.splice(At, 5, 0, ' ')
B.splice(At, 6, 0, 'world')
A.merge(B)
let binary = A.save()
let C = loadDoc(binary)
assert.deepEqual(C.value('_root', 'text'), ['text', 'hello world'])
assert.deepEqual(C.value('_root', 'text'), ['text', '1@aabbcc'])
assert.deepEqual(C.text(At), 'hell! world')
})
})

View file

@ -26,6 +26,8 @@ tinyvec = { version = "^1.5.1", features = ["alloc"] }
unicode-segmentation = "1.7.1"
serde = { version = "^1.0", features=["derive"] }
dot = { version = "0.1.4", optional = true }
js-sys = "^0.3"
wasm-bindgen = "^0.2"
[dependencies.web-sys]
version = "^0.3.55"

View file

@ -126,6 +126,13 @@ impl Automerge {
self.transaction.as_mut().unwrap()
}
pub fn fork(&mut self) -> Self {
self.ensure_transaction_closed();
let mut f = self.clone();
f.actor = None;
f
}
pub fn commit(&mut self, message: Option<String>, time: Option<i64>) -> Vec<ChangeHash> {
let tx = self.tx();
@ -630,10 +637,23 @@ impl Automerge {
Ok(delta)
}
fn duplicate_seq(&self, change: &Change) -> bool {
let mut dup = false;
if let Some(actor_index) = self.ops.m.actors.lookup(change.actor_id()) {
if let Some(s) = self.states.get(&actor_index) {
dup = s.len() >= change.seq as usize;
}
}
dup
}
pub fn apply_changes(&mut self, changes: &[Change]) -> Result<Patch, AutomergeError> {
self.ensure_transaction_closed();
for c in changes {
if !self.history_index.contains_key(&c.hash) {
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());
} else {
@ -804,15 +824,15 @@ impl Automerge {
}
/// Takes all the changes in `other` which are not in `self` and applies them
pub fn merge(&mut self, other: &mut Self) {
pub fn merge(&mut self, other: &mut Self) -> Result<Vec<ChangeHash>,AutomergeError> {
// TODO: Make this fallible and figure out how to do this transactionally
other.ensure_transaction_closed();
let changes = self
.get_changes_added(other)
.into_iter()
.cloned()
.collect::<Vec<_>>();
self.apply_changes(&changes).unwrap();
self.apply_changes(&changes)?;
Ok(self._get_heads())
}
pub fn save(&mut self) -> Result<Vec<u8>, AutomergeError> {
@ -1046,8 +1066,9 @@ impl Automerge {
.and_then(|index| self.history.get(*index))
}
pub fn get_changes_added<'a>(&mut self, other: &'a Self) -> Vec<&'a Change> {
pub fn get_changes_added<'a>(&mut self, other: &'a mut Self) -> Vec<&'a Change> {
self.ensure_transaction_closed();
other.ensure_transaction_closed();
self._get_changes_added(other)
}

View file

@ -1,5 +1,5 @@
use crate::decoding;
use crate::types::ScalarValue;
use crate::types::{ ActorId, ScalarValue};
use crate::value::DataType;
use thiserror::Error;
@ -17,6 +17,8 @@ pub enum AutomergeError {
InvalidSeq(u64),
#[error("index {0} is out of bounds")]
InvalidIndex(usize),
#[error("duplicate seq {0} found for actor {1}")]
DuplicateSeqNumber(u64,ActorId),
#[error("generic automerge error")]
Fail,
}
@ -33,6 +35,12 @@ impl From<decoding::Error> for AutomergeError {
}
}
impl From<AutomergeError> for wasm_bindgen::JsValue {
fn from(err: AutomergeError) -> Self {
js_sys::Error::new(&std::format!("{}", err)).into()
}
}
#[derive(Error, Debug)]
#[error("Invalid actor ID: {0}")]
pub struct InvalidActorId(pub String);