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:
parent
ec3f6cd69f
commit
80640ac799
6 changed files with 90 additions and 42 deletions
4
automerge-wasm/index.d.ts
vendored
4
automerge-wasm/index.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue