Compare commits

...

6 commits
main ... patch4

Author SHA1 Message Date
Orion Henry
932e93261c cleanup for PR, remove comments, document, clean up patch merge 2022-09-29 15:59:10 -05:00
Orion Henry
4860adfa52 fmt & clippy 2022-09-29 15:59:10 -05:00
Orion Henry
de80ca1657 update all tests 2022-09-29 15:59:10 -05:00
Orion Henry
323923c386 implement increment 2022-09-29 15:59:10 -05:00
Orion Henry
47fa3ae218 map and array insert, delete for apply() 2022-09-29 15:59:10 -05:00
Orion Henry
b5742315ef move op observer into transaction 2022-09-29 15:59:08 -05:00
23 changed files with 1166 additions and 637 deletions

View file

@ -170,7 +170,7 @@ pub unsafe extern "C" fn AMcommit(
if let Some(time) = time.as_ref() {
options.set_time(*time);
}
to_result(doc.commit_with::<()>(options))
to_result(doc.commit_with(options))
}
/// \memberof AMdoc

View file

@ -1,2 +1,17 @@
import { Automerge as VanillaAutomerge } from "automerge-types"
export * from "automerge-types"
export { default } from "automerge-types"
export class Automerge extends VanillaAutomerge {
// experimental api can go here
applyPatches(obj: any): any;
// override old methods that return automerge
clone(actor?: string): Automerge;
fork(actor?: string): Automerge;
forkAt(heads: Heads, actor?: string): Automerge;
}
export function create(actor?: Actor): Automerge;
export function load(data: Uint8Array, actor?: Actor): Automerge;

View file

@ -1,13 +1,14 @@
use crate::AutoCommit;
use automerge as am;
use automerge::transaction::Transactable;
use automerge::{Change, ChangeHash, Prop};
use js_sys::{Array, Object, Reflect, Uint8Array};
use js_sys::{Array, Function, Object, Reflect, Uint8Array};
use std::collections::{BTreeSet, HashSet};
use std::fmt::Display;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use crate::{ObjId, ScalarValue, Value};
use crate::{observer::Patch, ObjId, ScalarValue, Value};
pub(crate) struct JS(pub(crate) JsValue);
pub(crate) struct AR(pub(crate) Array);
@ -357,7 +358,7 @@ pub(crate) fn get_heads(heads: Option<Array>) -> Option<Vec<ChangeHash>> {
heads.ok()
}
pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
pub(crate) fn map_to_js(doc: &AutoCommit, obj: &ObjId) -> JsValue {
let keys = doc.keys(obj);
let map = Object::new();
for k in keys {
@ -383,7 +384,7 @@ pub(crate) fn map_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
map.into()
}
pub(crate) fn map_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
pub(crate) fn map_to_js_at(doc: &AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
let keys = doc.keys(obj);
let map = Object::new();
for k in keys {
@ -409,7 +410,7 @@ pub(crate) fn map_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHas
map.into()
}
pub(crate) fn list_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
pub(crate) fn list_to_js(doc: &AutoCommit, obj: &ObjId) -> JsValue {
let len = doc.length(obj);
let array = Array::new();
for i in 0..len {
@ -435,7 +436,7 @@ pub(crate) fn list_to_js(doc: &am::AutoCommit, obj: &ObjId) -> JsValue {
array.into()
}
pub(crate) fn list_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
pub(crate) fn list_to_js_at(doc: &AutoCommit, obj: &ObjId, heads: &[ChangeHash]) -> JsValue {
let len = doc.length(obj);
let array = Array::new();
for i in 0..len {
@ -460,3 +461,143 @@ pub(crate) fn list_to_js_at(doc: &am::AutoCommit, obj: &ObjId, heads: &[ChangeHa
}
array.into()
}
/*
pub(crate) fn export_values<'a, V: Iterator<Item = Value<'a>>>(val: V) -> Array {
val.map(|v| export_value(&v)).collect()
}
*/
pub(crate) fn export_value(val: &Value<'_>) -> JsValue {
match val {
Value::Object(o) if o == &am::ObjType::Map || o == &am::ObjType::Table => {
Object::new().into()
}
Value::Object(_) => Array::new().into(),
Value::Scalar(v) => ScalarValue(v.clone()).into(),
}
}
pub(crate) fn apply_patch(obj: JsValue, patch: &Patch) -> Result<JsValue, JsValue> {
apply_patch2(obj, patch, 0)
}
pub(crate) fn apply_patch2(obj: JsValue, patch: &Patch, depth: usize) -> Result<JsValue, JsValue> {
match (js_to_map_seq(&obj)?, patch.path().get(depth)) {
(JsObj::Map(o), Some(Prop::Map(key))) => {
let sub_obj = Reflect::get(&obj, &key.into())?;
let new_value = apply_patch2(sub_obj, patch, depth + 1)?;
let result =
Reflect::construct(&o.constructor(), &Array::new())?.dyn_into::<Object>()?;
let result = Object::assign(&result, &o).into();
Reflect::set(&result, &key.into(), &new_value)?;
Ok(result)
}
(JsObj::Seq(a), Some(Prop::Seq(index))) => {
let index = JsValue::from_f64(*index as f64);
let sub_obj = Reflect::get(&obj, &index)?;
let new_value = apply_patch2(sub_obj, patch, depth + 1)?;
let result = Reflect::construct(&a.constructor(), &a)?;
//web_sys::console::log_2(&format!("NEW VAL {}: ", tmpi).into(), &new_value);
Reflect::set(&result, &index, &new_value)?;
Ok(result)
}
(JsObj::Map(o), None) => {
let result =
Reflect::construct(&o.constructor(), &Array::new())?.dyn_into::<Object>()?;
let result = Object::assign(&result, &o);
match patch {
Patch::PutMap { key, value, .. } => {
let result = result.into();
Reflect::set(&result, &key.into(), &export_value(value))?;
Ok(result)
}
Patch::DeleteMap { key, .. } => {
Reflect::delete_property(&result, &key.into())?;
Ok(result.into())
}
Patch::Increment { prop, value, .. } => {
let result = result.into();
if let Prop::Map(key) = prop {
let key = key.into();
let old_val = Reflect::get(&o, &key)?;
if let Some(old) = old_val.as_f64() {
Reflect::set(&result, &key, &JsValue::from(old + *value as f64))?;
Ok(result)
} else {
Err(to_js_err("cant increment a non number value"))
}
} else {
Err(to_js_err("cant increment an index on a map"))
}
}
Patch::Insert { .. } => Err(to_js_err("cannot insert into map")),
Patch::DeleteSeq { .. } => Err(to_js_err("cannot splice a map")),
Patch::PutSeq { .. } => Err(to_js_err("cannot array index a map")),
}
}
(JsObj::Seq(a), None) => {
match patch {
Patch::PutSeq { index, value, .. } => {
let result = Reflect::construct(&a.constructor(), &a)?;
Reflect::set(&result, &(*index as f64).into(), &export_value(value))?;
Ok(result)
}
Patch::DeleteSeq { index, .. } => {
let result = &a.dyn_into::<Array>()?;
let mut f = |_, i, _| i != *index as u32;
let result = result.filter(&mut f);
Ok(result.into())
}
Patch::Insert { index, values, .. } => {
let from = Reflect::get(&a.constructor().into(), &"from".into())?
.dyn_into::<Function>()?;
let result = from.call1(&JsValue::undefined(), &a)?.dyn_into::<Array>()?;
// TODO: should be one function call
for (i, v) in values.iter().enumerate() {
result.splice(*index as u32 + i as u32, 0, &export_value(v));
}
Ok(result.into())
}
Patch::Increment { prop, value, .. } => {
let result = Reflect::construct(&a.constructor(), &a)?;
if let Prop::Seq(index) = prop {
let index = (*index as f64).into();
let old_val = Reflect::get(&a, &index)?;
if let Some(old) = old_val.as_f64() {
Reflect::set(&result, &index, &JsValue::from(old + *value as f64))?;
Ok(result)
} else {
Err(to_js_err("cant increment a non number value"))
}
} else {
Err(to_js_err("cant increment a key on a seq"))
}
}
Patch::DeleteMap { .. } => Err(to_js_err("cannot delete from a seq")),
Patch::PutMap { .. } => Err(to_js_err("cannot set key in seq")),
}
}
(_, _) => Err(to_js_err(format!(
"object/patch missmatch {:?} depth={:?}",
patch, depth
))),
}
}
#[derive(Debug)]
enum JsObj {
Map(Object),
Seq(Array),
}
fn js_to_map_seq(value: &JsValue) -> Result<JsObj, JsValue> {
if let Ok(array) = value.clone().dyn_into::<Array>() {
Ok(JsObj::Seq(array))
} else if let Ok(obj) = value.clone().dyn_into::<Object>() {
Ok(JsObj::Map(obj))
} else {
Err(to_js_err("obj is not Object or Array"))
}
}

View file

@ -28,10 +28,7 @@
#![allow(clippy::unused_unit)]
use am::transaction::CommitOptions;
use am::transaction::Transactable;
use am::ApplyOptions;
use automerge as am;
use automerge::Patch;
use automerge::VecOpObserver;
use automerge::{Change, ObjId, Prop, Value, ROOT};
use js_sys::{Array, Object, Uint8Array};
use serde::Serialize;
@ -40,12 +37,15 @@ use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
mod interop;
mod observer;
mod sync;
mod value;
use observer::Observer;
use interop::{
get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at, to_js_err,
to_objtype, to_prop, AR, JS,
apply_patch, get_heads, js_get, js_set, list_to_js, list_to_js_at, map_to_js, map_to_js_at,
to_js_err, to_objtype, to_prop, AR, JS,
};
use sync::SyncState;
use value::{datatype, ScalarValue};
@ -57,6 +57,8 @@ macro_rules! log {
};
}
type AutoCommit = am::AutoCommitWithObs<Observer>;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
@ -64,40 +66,24 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
#[derive(Debug)]
pub struct Automerge {
doc: automerge::AutoCommit,
observer: Option<VecOpObserver>,
doc: AutoCommit,
}
#[wasm_bindgen]
impl Automerge {
pub fn new(actor: Option<String>) -> Result<Automerge, JsValue> {
let mut automerge = automerge::AutoCommit::new();
let mut doc = AutoCommit::default();
if let Some(a) = actor {
let a = automerge::ActorId::from(hex::decode(a).map_err(to_js_err)?.to_vec());
automerge.set_actor(a);
}
Ok(Automerge {
doc: automerge,
observer: None,
})
}
fn ensure_transaction_closed(&mut self) {
if self.doc.pending_ops() > 0 {
let mut opts = CommitOptions::default();
if let Some(observer) = self.observer.as_mut() {
opts.set_op_observer(observer);
}
self.doc.commit_with(opts);
doc.set_actor(a);
}
Ok(Automerge { doc })
}
#[allow(clippy::should_implement_trait)]
pub fn clone(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
self.ensure_transaction_closed();
let mut automerge = Automerge {
doc: self.doc.clone(),
observer: None,
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
@ -107,10 +93,8 @@ impl Automerge {
}
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> {
self.ensure_transaction_closed();
let mut automerge = Automerge {
doc: self.doc.fork(),
observer: None,
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
@ -124,7 +108,6 @@ impl Automerge {
let deps: Vec<_> = JS(heads).try_into()?;
let mut automerge = Automerge {
doc: self.doc.fork_at(&deps)?,
observer: None,
};
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
@ -148,21 +131,12 @@ impl Automerge {
if let Some(time) = time {
commit_opts.set_time(time as i64);
}
if let Some(observer) = self.observer.as_mut() {
commit_opts.set_op_observer(observer);
}
let hash = self.doc.commit_with(commit_opts);
JsValue::from_str(&hex::encode(&hash.0))
}
pub fn merge(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
self.ensure_transaction_closed();
let options = if let Some(observer) = self.observer.as_mut() {
ApplyOptions::default().with_op_observer(observer)
} else {
ApplyOptions::default()
};
let heads = self.doc.merge_with(&mut other.doc, options)?;
let heads = self.doc.merge(&mut other.doc)?;
let heads: Array = heads
.iter()
.map(|h| JsValue::from_str(&hex::encode(&h.0)))
@ -454,84 +428,30 @@ impl Automerge {
pub fn enable_patches(&mut self, enable: JsValue) -> Result<(), JsValue> {
let enable = enable
.as_bool()
.ok_or_else(|| to_js_err("expected boolean"))?;
if enable {
if self.observer.is_none() {
self.observer = Some(VecOpObserver::default());
}
} else {
self.observer = None;
}
.ok_or_else(|| to_js_err("must pass a bool to enable_patches"))?;
self.doc.observer().enable(enable);
Ok(())
}
#[wasm_bindgen(js_name = applyPatches)]
pub fn apply_patches(&mut self, mut object: JsValue) -> Result<JsValue, JsValue> {
let patches = self.doc.observer().take_patches();
for p in patches {
object = apply_patch(object, &p)?;
}
Ok(object)
}
#[wasm_bindgen(js_name = popPatches)]
pub fn pop_patches(&mut self) -> Result<Array, JsValue> {
// transactions send out observer updates as they occur, not waiting for them to be
// committed.
// If we pop the patches then we won't be able to revert them.
self.ensure_transaction_closed();
let patches = self
.observer
.as_mut()
.map_or_else(Vec::new, |o| o.take_patches());
let patches = self.doc.observer().take_patches();
let result = Array::new();
for p in patches {
let patch = Object::new();
match p {
Patch::Put {
obj,
key,
value,
conflict,
} => {
js_set(&patch, "action", "put")?;
js_set(&patch, "obj", obj.to_string())?;
js_set(&patch, "key", key)?;
match value {
(Value::Object(obj_type), obj_id) => {
js_set(&patch, "datatype", obj_type.to_string())?;
js_set(&patch, "value", obj_id.to_string())?;
}
(Value::Scalar(value), _) => {
js_set(&patch, "datatype", datatype(&value))?;
js_set(&patch, "value", ScalarValue(value))?;
}
};
js_set(&patch, "conflict", conflict)?;
}
Patch::Insert { obj, index, value } => {
js_set(&patch, "action", "insert")?;
js_set(&patch, "obj", obj.to_string())?;
js_set(&patch, "key", index as f64)?;
match value {
(Value::Object(obj_type), obj_id) => {
js_set(&patch, "datatype", obj_type.to_string())?;
js_set(&patch, "value", obj_id.to_string())?;
}
(Value::Scalar(value), _) => {
js_set(&patch, "datatype", datatype(&value))?;
js_set(&patch, "value", ScalarValue(value))?;
}
};
}
Patch::Increment { obj, key, value } => {
js_set(&patch, "action", "increment")?;
js_set(&patch, "obj", obj.to_string())?;
js_set(&patch, "key", key)?;
js_set(&patch, "value", value.0)?;
}
Patch::Delete { obj, key } => {
js_set(&patch, "action", "delete")?;
js_set(&patch, "obj", obj.to_string())?;
js_set(&patch, "key", key)?;
}
}
result.push(&patch);
result.push(&p.try_into()?);
}
Ok(result)
}
@ -553,51 +473,31 @@ impl Automerge {
}
pub fn save(&mut self) -> Uint8Array {
self.ensure_transaction_closed();
Uint8Array::from(self.doc.save().as_slice())
}
#[wasm_bindgen(js_name = saveIncremental)]
pub fn save_incremental(&mut self) -> Uint8Array {
self.ensure_transaction_closed();
let bytes = self.doc.save_incremental();
Uint8Array::from(bytes.as_slice())
}
#[wasm_bindgen(js_name = loadIncremental)]
pub fn load_incremental(&mut self, data: Uint8Array) -> Result<f64, JsValue> {
self.ensure_transaction_closed();
let data = data.to_vec();
let options = if let Some(observer) = self.observer.as_mut() {
ApplyOptions::default().with_op_observer(observer)
} else {
ApplyOptions::default()
};
let len = self
.doc
.load_incremental_with(&data, options)
.map_err(to_js_err)?;
let len = self.doc.load_incremental(&data).map_err(to_js_err)?;
Ok(len as f64)
}
#[wasm_bindgen(js_name = applyChanges)]
pub fn apply_changes(&mut self, changes: JsValue) -> Result<(), JsValue> {
self.ensure_transaction_closed();
let changes: Vec<_> = JS(changes).try_into()?;
let options = if let Some(observer) = self.observer.as_mut() {
ApplyOptions::default().with_op_observer(observer)
} else {
ApplyOptions::default()
};
self.doc
.apply_changes_with(changes, options)
.map_err(to_js_err)?;
self.doc.apply_changes(changes).map_err(to_js_err)?;
Ok(())
}
#[wasm_bindgen(js_name = getChanges)]
pub fn get_changes(&mut self, have_deps: JsValue) -> Result<Array, JsValue> {
self.ensure_transaction_closed();
let deps: Vec<_> = JS(have_deps).try_into()?;
let changes = self.doc.get_changes(&deps)?;
let changes: Array = changes
@ -609,7 +509,7 @@ impl Automerge {
#[wasm_bindgen(js_name = getChangeByHash)]
pub fn get_change_by_hash(&mut self, hash: JsValue) -> Result<JsValue, JsValue> {
self.ensure_transaction_closed();
self.doc.ensure_transaction_closed();
let hash = serde_wasm_bindgen::from_value(hash).map_err(to_js_err)?;
let change = self.doc.get_change_by_hash(&hash);
if let Some(c) = change {
@ -621,7 +521,6 @@ impl Automerge {
#[wasm_bindgen(js_name = getChangesAdded)]
pub fn get_changes_added(&mut self, other: &mut Automerge) -> Result<Array, JsValue> {
self.ensure_transaction_closed();
let changes = self.doc.get_changes_added(&mut other.doc);
let changes: Array = changes
.iter()
@ -632,7 +531,6 @@ impl Automerge {
#[wasm_bindgen(js_name = getHeads)]
pub fn get_heads(&mut self) -> Array {
self.ensure_transaction_closed();
let heads = self.doc.get_heads();
let heads: Array = heads
.iter()
@ -649,7 +547,6 @@ impl Automerge {
#[wasm_bindgen(js_name = getLastLocalChange)]
pub fn get_last_local_change(&mut self) -> Result<JsValue, JsValue> {
self.ensure_transaction_closed();
if let Some(change) = self.doc.get_last_local_change() {
Ok(Uint8Array::from(change.raw_bytes()).into())
} else {
@ -658,13 +555,11 @@ impl Automerge {
}
pub fn dump(&mut self) {
self.ensure_transaction_closed();
self.doc.dump()
}
#[wasm_bindgen(js_name = getMissingDeps)]
pub fn get_missing_deps(&mut self, heads: Option<Array>) -> Result<Array, JsValue> {
self.ensure_transaction_closed();
let heads = get_heads(heads).unwrap_or_default();
let deps = self.doc.get_missing_deps(&heads);
let deps: Array = deps
@ -680,23 +575,16 @@ impl Automerge {
state: &mut SyncState,
message: Uint8Array,
) -> Result<(), JsValue> {
self.ensure_transaction_closed();
let message = message.to_vec();
let message = am::sync::Message::decode(message.as_slice()).map_err(to_js_err)?;
let options = if let Some(observer) = self.observer.as_mut() {
ApplyOptions::default().with_op_observer(observer)
} else {
ApplyOptions::default()
};
self.doc
.receive_sync_message_with(&mut state.0, message, options)
.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<JsValue, JsValue> {
self.ensure_transaction_closed();
if let Some(message) = self.doc.generate_sync_message(&mut state.0) {
Ok(Uint8Array::from(message.encode().as_slice()).into())
} else {
@ -856,17 +744,12 @@ pub fn init(actor: Option<String>) -> Result<Automerge, JsValue> {
#[wasm_bindgen(js_name = loadDoc)]
pub fn load(data: Uint8Array, actor: Option<String>) -> Result<Automerge, JsValue> {
let data = data.to_vec();
let observer = None;
let options = ApplyOptions::<()>::default();
let mut automerge = am::AutoCommit::load_with(&data, options).map_err(to_js_err)?;
let mut doc = AutoCommit::load(&data).map_err(to_js_err)?;
if let Some(s) = actor {
let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec());
automerge.set_actor(actor);
doc.set_actor(actor);
}
Ok(Automerge {
doc: automerge,
observer,
})
Ok(Automerge { doc })
}
#[wasm_bindgen(js_name = encodeChange)]

View file

@ -0,0 +1,302 @@
#![allow(dead_code)]
use crate::interop::{export_value, js_set};
use automerge::{ObjId, OpObserver, Parents, Prop, Value};
use js_sys::{Array, Object};
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Default)]
pub(crate) struct Observer {
enabled: bool,
patches: Vec<Patch>,
}
impl Observer {
pub(crate) fn take_patches(&mut self) -> Vec<Patch> {
std::mem::take(&mut self.patches)
}
pub(crate) fn enable(&mut self, enable: bool) {
if self.enabled && !enable {
self.patches.truncate(0)
}
self.enabled = enable;
}
}
#[derive(Debug, Clone)]
pub(crate) enum Patch {
PutMap {
obj: ObjId,
path: Vec<Prop>,
key: String,
value: Value<'static>,
conflict: bool,
},
PutSeq {
obj: ObjId,
path: Vec<Prop>,
index: usize,
value: Value<'static>,
conflict: bool,
},
Insert {
obj: ObjId,
path: Vec<Prop>,
index: usize,
values: Vec<Value<'static>>,
},
Increment {
obj: ObjId,
path: Vec<Prop>,
prop: Prop,
value: i64,
},
DeleteMap {
obj: ObjId,
path: Vec<Prop>,
key: String,
},
DeleteSeq {
obj: ObjId,
path: Vec<Prop>,
index: usize,
length: usize,
},
}
impl OpObserver for Observer {
fn insert(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
index: usize,
tagged_value: (Value<'_>, ObjId),
) {
if self.enabled {
if let Some(Patch::Insert {
obj: tail_obj,
index: tail_index,
values,
..
}) = self.patches.last_mut()
{
if tail_obj == &obj && *tail_index + values.len() == index {
values.push(tagged_value.0.to_owned());
return;
}
}
let path = parents.path().into_iter().map(|p| p.1).collect();
let value = tagged_value.0.to_owned();
let patch = Patch::Insert {
path,
obj,
index,
values: vec![value],
};
self.patches.push(patch);
}
}
fn put(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
prop: Prop,
tagged_value: (Value<'_>, ObjId),
conflict: bool,
) {
if self.enabled {
let path = parents.path().into_iter().map(|p| p.1).collect();
let value = tagged_value.0.to_owned();
let patch = match prop {
Prop::Map(key) => Patch::PutMap {
path,
obj,
key,
value,
conflict,
},
Prop::Seq(index) => Patch::PutSeq {
path,
obj,
index,
value,
conflict,
},
};
self.patches.push(patch);
}
}
fn increment(
&mut self,
mut parents: Parents<'_>,
obj: ObjId,
prop: Prop,
tagged_value: (i64, ObjId),
) {
if self.enabled {
let path = parents.path().into_iter().map(|p| p.1).collect();
let value = tagged_value.0;
self.patches.push(Patch::Increment {
path,
obj,
prop,
value,
})
}
}
fn delete(&mut self, mut parents: Parents<'_>, obj: ObjId, prop: Prop) {
if self.enabled {
let path = parents.path().into_iter().map(|p| p.1).collect();
let patch = match prop {
Prop::Map(key) => Patch::DeleteMap { path, obj, key },
Prop::Seq(index) => Patch::DeleteSeq {
path,
obj,
index,
length: 1,
},
};
self.patches.push(patch)
}
}
fn merge(&mut self, other: &Self) {
self.patches.extend_from_slice(other.patches.as_slice())
}
fn branch(&self) -> Self {
Observer {
patches: vec![],
enabled: self.enabled,
}
}
}
fn prop_to_js(p: &Prop) -> JsValue {
match p {
Prop::Map(key) => JsValue::from_str(key),
Prop::Seq(index) => JsValue::from_f64(*index as f64),
}
}
fn export_path(path: &[Prop], end: &Prop) -> Array {
let result = Array::new();
for p in path {
result.push(&prop_to_js(p));
}
result.push(&prop_to_js(end));
result
}
impl Patch {
pub(crate) fn path(&self) -> &[Prop] {
match &self {
Self::PutMap { path, .. } => path.as_slice(),
Self::PutSeq { path, .. } => path.as_slice(),
Self::Increment { path, .. } => path.as_slice(),
Self::Insert { path, .. } => path.as_slice(),
Self::DeleteMap { path, .. } => path.as_slice(),
Self::DeleteSeq { path, .. } => path.as_slice(),
}
}
}
impl TryFrom<Patch> for JsValue {
type Error = JsValue;
fn try_from(p: Patch) -> Result<Self, Self::Error> {
let result = Object::new();
match p {
Patch::PutMap {
path,
key,
value,
conflict,
..
} => {
js_set(&result, "action", "put")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
js_set(&result, "value", export_value(&value))?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::PutSeq {
path,
index,
value,
conflict,
..
} => {
js_set(&result, "action", "put")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(&result, "value", export_value(&value))?;
js_set(&result, "conflict", &JsValue::from_bool(conflict))?;
Ok(result.into())
}
Patch::Insert {
path,
index,
values,
..
} => {
js_set(&result, "action", "splice")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
js_set(
&result,
"values",
values.iter().map(export_value).collect::<Array>(),
)?;
Ok(result.into())
}
Patch::Increment {
path, prop, value, ..
} => {
js_set(&result, "action", "inc")?;
js_set(&result, "path", export_path(path.as_slice(), &prop))?;
js_set(&result, "value", &JsValue::from_f64(value as f64))?;
Ok(result.into())
}
Patch::DeleteMap { path, key, .. } => {
js_set(&result, "action", "del")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Map(key)),
)?;
Ok(result.into())
}
Patch::DeleteSeq {
path,
index,
length,
..
} => {
js_set(&result, "action", "del")?;
js_set(
&result,
"path",
export_path(path.as_slice(), &Prop::Seq(index)),
)?;
if length > 1 {
js_set(&result, "length", length)?;
}
Ok(result.into())
}
}
}
}

View file

@ -0,0 +1,100 @@
import { describe, it } from 'mocha';
//@ts-ignore
import assert from 'assert'
//@ts-ignore
import init, { create, load } from '..'
describe('Automerge', () => {
describe('Patch Apply', () => {
it('apply nested sets on maps', () => {
let start : any = { hello: { mellow: { yellow: "world", x: 1 }, y : 2 } }
let doc1 = create()
doc1.putObject("/", "hello", start.hello);
let mat = doc1.materialize("/")
let doc2 = create()
doc2.enablePatches(true)
doc2.merge(doc1)
let base = doc2.applyPatches({})
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
doc2.delete("/hello/mellow", "yellow");
delete start.hello.mellow.yellow;
base = doc2.applyPatches(base)
mat = doc2.materialize("/")
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
})
it('apply patches on lists', () => {
//let start = { list: [1,2,3,4,5,6] }
let start = { list: [1,2,3,4] }
let doc1 = create()
doc1.putObject("/", "list", start.list);
let mat = doc1.materialize("/")
let doc2 = create()
doc2.enablePatches(true)
doc2.merge(doc1)
mat = doc1.materialize("/")
let base = doc2.applyPatches({})
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
doc2.delete("/list", 3);
start.list.splice(3,1)
base = doc2.applyPatches(base)
assert.deepEqual(base, start)
})
it('apply patches on lists of lists of lists', () => {
let start = { list:
[
[
[ 1, 2, 3, 4, 5, 6],
[ 7, 8, 9,10,11,12],
],
[
[ 7, 8, 9,10,11,12],
[ 1, 2, 3, 4, 5, 6],
]
]
}
let doc1 = create()
doc1.enablePatches(true)
doc1.putObject("/", "list", start.list);
let mat = doc1.materialize("/")
let base = doc1.applyPatches({})
assert.deepEqual(mat, start)
doc1.delete("/list/0/1", 3)
start.list[0][1].splice(3,1)
doc1.delete("/list/0", 0)
start.list[0].splice(0,1)
mat = doc1.materialize("/")
base = doc1.applyPatches(base)
assert.deepEqual(mat, start)
assert.deepEqual(base, start)
})
it('large inserts should make one splice patch', () => {
let doc1 = create()
doc1.enablePatches(true)
doc1.putObject("/", "list", "abc");
let patches = doc1.popPatches()
assert.deepEqual( patches, [
{ action: 'put', conflict: false, path: [ 'list' ], value: [] },
{ action: 'splice', path: [ 'list', 0 ], values: [ 'a', 'b', 'c' ] }])
})
})
})
// FIXME: handle conflicts correctly on apply
// TODO: squash puts
// TODO: merge deletes
// TODO: elide `conflict: false`

View file

@ -506,7 +506,7 @@ describe('Automerge', () => {
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'hello', value: 'world', datatype: 'str', conflict: false }
{ action: 'put', path: ['hello'], value: 'world', conflict: false }
])
doc1.free()
doc2.free()
@ -518,9 +518,9 @@ describe('Automerge', () => {
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'birds', value: '1@aaaa', datatype: 'map', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 'friday', value: '2@aaaa', datatype: 'map', conflict: false },
{ action: 'put', obj: '2@aaaa', key: 'robins', value: 3, datatype: 'int', conflict: false }
{ action: 'put', path: [ 'birds' ], value: {}, conflict: false },
{ action: 'put', path: [ 'birds', 'friday' ], value: {}, conflict: false },
{ action: 'put', path: [ 'birds', 'friday', 'robins' ], value: 3, conflict: false},
])
doc1.free()
doc2.free()
@ -534,8 +534,8 @@ describe('Automerge', () => {
doc1.delete('_root', 'favouriteBird')
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'favouriteBird', value: 'Robin', datatype: 'str', conflict: false },
{ action: 'delete', obj: '_root', key: 'favouriteBird' }
{ action: 'put', path: [ 'favouriteBird' ], value: 'Robin', conflict: false },
{ action: 'del', path: [ 'favouriteBird' ] }
])
doc1.free()
doc2.free()
@ -547,9 +547,8 @@ describe('Automerge', () => {
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'birds', value: '1@aaaa', datatype: 'list', conflict: false },
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'Goldfinch', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 1, value: 'Chaffinch', datatype: 'str' }
{ action: 'put', path: [ 'birds' ], value: [], conflict: false },
{ action: 'splice', path: [ 'birds', 0 ], values: ['Goldfinch', 'Chaffinch'] },
])
doc1.free()
doc2.free()
@ -563,9 +562,9 @@ describe('Automerge', () => {
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'insert', obj: '1@aaaa', key: 0, value: '2@aaaa', datatype: 'map' },
{ action: 'put', obj: '2@aaaa', key: 'species', value: 'Goldfinch', datatype: 'str', conflict: false },
{ action: 'put', obj: '2@aaaa', key: 'count', value: 3, datatype: 'int', conflict: false }
{ action: 'splice', path: [ 'birds', 0 ], values: [{}] },
{ action: 'put', path: [ 'birds', 0, 'species' ], value: 'Goldfinch', conflict: false },
{ action: 'put', path: [ 'birds', 0, 'count', ], value: 3, conflict: false }
])
doc1.free()
doc2.free()
@ -582,8 +581,8 @@ describe('Automerge', () => {
assert.deepEqual(doc1.getWithType('1@aaaa', 0), ['str', 'Chaffinch'])
assert.deepEqual(doc1.getWithType('1@aaaa', 1), ['str', 'Greenfinch'])
assert.deepEqual(doc2.popPatches(), [
{ action: 'delete', obj: '1@aaaa', key: 0 },
{ action: 'insert', obj: '1@aaaa', key: 1, value: 'Greenfinch', datatype: 'str' }
{ action: 'del', path: ['birds', 0] },
{ action: 'splice', path: ['birds', 1], values: ['Greenfinch'] }
])
doc1.free()
doc2.free()
@ -608,16 +607,11 @@ describe('Automerge', () => {
assert.deepEqual([0, 1, 2, 3].map(i => (doc3.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd'])
assert.deepEqual([0, 1, 2, 3].map(i => (doc4.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd'])
assert.deepEqual(doc3.popPatches(), [
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'c', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 1, value: 'd', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'a', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 1, value: 'b', datatype: 'str' }
{ action: 'splice', path: ['values', 0], values:['c','d'] },
{ action: 'splice', path: ['values', 0], values:['a','b'] },
])
assert.deepEqual(doc4.popPatches(), [
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'a', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 1, value: 'b', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' }
{ action: 'splice', path: ['values',0], values:['a','b','c','d'] },
])
doc1.free(); doc2.free(); doc3.free(); doc4.free()
})
@ -641,16 +635,11 @@ describe('Automerge', () => {
assert.deepEqual([0, 1, 2, 3, 4, 5].map(i => (doc3.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd', 'e', 'f'])
assert.deepEqual([0, 1, 2, 3, 4, 5].map(i => (doc4.getWithType('1@aaaa', i) || [])[1]), ['a', 'b', 'c', 'd', 'e', 'f'])
assert.deepEqual(doc3.popPatches(), [
{ action: 'insert', obj: '1@aaaa', key: 2, value: 'e', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 3, value: 'f', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' }
{ action: 'splice', path: ['values', 2], values: ['e','f'] },
{ action: 'splice', path: ['values', 2], values: ['c','d'] },
])
assert.deepEqual(doc4.popPatches(), [
{ action: 'insert', obj: '1@aaaa', key: 2, value: 'c', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 3, value: 'd', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 4, value: 'e', datatype: 'str' },
{ action: 'insert', obj: '1@aaaa', key: 5, value: 'f', datatype: 'str' }
{ action: 'splice', path: ['values', 2], values: ['c','d','e','f'] },
])
doc1.free(); doc2.free(); doc3.free(); doc4.free()
})
@ -669,12 +658,12 @@ describe('Automerge', () => {
assert.deepEqual(doc4.getWithType('_root', 'bird'), ['str', 'Goldfinch'])
assert.deepEqual(doc4.getAll('_root', 'bird'), [['str', 'Greenfinch', '1@aaaa'], ['str', 'Goldfinch', '1@bbbb']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Greenfinch', datatype: 'str', conflict: false },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Greenfinch', conflict: false },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
])
assert.deepEqual(doc4.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
])
doc1.free(); doc2.free(); doc3.free(); doc4.free()
})
@ -704,16 +693,16 @@ describe('Automerge', () => {
['str', 'Greenfinch', '1@aaaa'], ['str', 'Chaffinch', '1@bbbb'], ['str', 'Goldfinch', '1@cccc']
])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Chaffinch', datatype: 'str', conflict: true },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Chaffinch', conflict: true },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
])
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: true }
])
doc1.free(); doc2.free(); doc3.free()
})
@ -730,9 +719,9 @@ describe('Automerge', () => {
doc3.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Goldfinch', '2@aaaa']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Greenfinch', datatype: 'str', conflict: false },
{ action: 'put', obj: '_root', key: 'bird', value: 'Chaffinch', datatype: 'str', conflict: true },
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
{ action: 'put', path: ['bird'], value: 'Greenfinch', conflict: false },
{ action: 'put', path: ['bird'], value: 'Chaffinch', conflict: true },
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
])
doc1.free(); doc2.free(); doc3.free()
})
@ -753,10 +742,10 @@ describe('Automerge', () => {
assert.deepEqual(doc2.getWithType('_root', 'bird'), ['str', 'Goldfinch'])
assert.deepEqual(doc2.getAll('_root', 'bird'), [['str', 'Goldfinch', '2@aaaa']])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
])
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Goldfinch', datatype: 'str', conflict: false }
{ action: 'put', path: ['bird'], value: 'Goldfinch', conflict: false }
])
doc1.free(); doc2.free()
})
@ -780,12 +769,12 @@ describe('Automerge', () => {
assert.deepEqual(doc4.getWithType('1@aaaa', 0), ['str', 'Redwing'])
assert.deepEqual(doc4.getAll('1@aaaa', 0), [['str', 'Song Thrush', '4@aaaa'], ['str', 'Redwing', '4@bbbb']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Song Thrush', datatype: 'str', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: true }
{ action: 'put', path: ['birds',0], value: 'Song Thrush', conflict: false },
{ action: 'put', path: ['birds',0], value: 'Redwing', conflict: true }
])
assert.deepEqual(doc4.popPatches(), [
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Redwing', datatype: 'str', conflict: true }
{ action: 'put', path: ['birds',0], value: 'Redwing', conflict: false },
{ action: 'put', path: ['birds',0], value: 'Redwing', conflict: true }
])
doc1.free(); doc2.free(); doc3.free(); doc4.free()
})
@ -811,16 +800,16 @@ describe('Automerge', () => {
assert.deepEqual(doc4.getAll('1@aaaa', 0), [['str', 'Ring-necked parakeet', '5@bbbb']])
assert.deepEqual(doc4.getAll('1@aaaa', 2), [['str', 'Song Thrush', '6@aaaa'], ['str', 'Redwing', '6@bbbb']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'delete', obj: '1@aaaa', key: 0 },
{ action: 'put', obj: '1@aaaa', key: 1, value: 'Song Thrush', datatype: 'str', conflict: false },
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str' },
{ action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: true }
{ action: 'del', path: ['birds',0], },
{ action: 'put', path: ['birds',1], value: 'Song Thrush', conflict: false },
{ action: 'splice', path: ['birds',0], values: ['Ring-necked parakeet'] },
{ action: 'put', path: ['birds',2], value: 'Redwing', conflict: true }
])
assert.deepEqual(doc4.popPatches(), [
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 0, value: 'Ring-necked parakeet', datatype: 'str', conflict: false },
{ action: 'put', obj: '1@aaaa', key: 2, value: 'Redwing', datatype: 'str', conflict: true }
{ action: 'put', path: ['birds',0], value: 'Ring-necked parakeet', conflict: false },
{ action: 'put', path: ['birds',2], value: 'Redwing', conflict: false },
{ action: 'put', path: ['birds',0], value: 'Ring-necked parakeet', conflict: false },
{ action: 'put', path: ['birds',2], value: 'Redwing', conflict: true }
])
doc1.free(); doc2.free(); doc3.free(); doc4.free()
})
@ -837,14 +826,14 @@ describe('Automerge', () => {
doc3.loadIncremental(change2)
assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Robin', '1@aaaa'], ['str', 'Wren', '1@bbbb']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Robin', datatype: 'str', conflict: false },
{ action: 'put', obj: '_root', key: 'bird', value: 'Wren', datatype: 'str', conflict: true }
{ action: 'put', path: ['bird'], value: 'Robin', conflict: false },
{ action: 'put', path: ['bird'], value: 'Wren', conflict: true }
])
doc3.loadIncremental(change3)
assert.deepEqual(doc3.getWithType('_root', 'bird'), ['str', 'Robin'])
assert.deepEqual(doc3.getAll('_root', 'bird'), [['str', 'Robin', '1@aaaa']])
assert.deepEqual(doc3.popPatches(), [
{ action: 'put', obj: '_root', key: 'bird', value: 'Robin', datatype: 'str', conflict: false }
{ action: 'put', path: ['bird'], value: 'Robin', conflict: false }
])
doc1.free(); doc2.free(); doc3.free()
})
@ -860,26 +849,25 @@ describe('Automerge', () => {
doc2.loadIncremental(change1)
assert.deepEqual(doc1.getAll('_root', 'birds'), [['list', '1@aaaa'], ['map', '1@bbbb']])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'birds', value: '1@bbbb', datatype: 'map', conflict: true },
{ action: 'put', obj: '1@bbbb', key: 'Sparrowhawk', value: 1, datatype: 'int', conflict: false }
{ action: 'put', path: ['birds'], value: {}, conflict: true },
{ action: 'put', path: ['birds', 'Sparrowhawk'], value: 1, conflict: false }
])
assert.deepEqual(doc2.getAll('_root', 'birds'), [['list', '1@aaaa'], ['map', '1@bbbb']])
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'birds', value: '1@bbbb', datatype: 'map', conflict: true },
{ action: 'insert', obj: '1@aaaa', key: 0, value: 'Parakeet', datatype: 'str' }
{ action: 'put', path: ['birds'], value: {}, conflict: true },
{ action: 'splice', path: ['birds',0], values: ['Parakeet'] }
])
doc1.free(); doc2.free()
})
it('should support date objects', () => {
// FIXME: either use Date objects or use numbers consistently
const doc1 = create('aaaa'), doc2 = create('bbbb'), now = new Date()
doc1.put('_root', 'createdAt', now.getTime(), 'timestamp')
doc1.put('_root', 'createdAt', now)
doc2.enablePatches(true)
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.getWithType('_root', 'createdAt'), ['timestamp', now])
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'createdAt', value: now, datatype: 'timestamp', conflict: false }
{ action: 'put', path: ['createdAt'], value: now, conflict: false }
])
doc1.free(); doc2.free()
})
@ -894,11 +882,11 @@ describe('Automerge', () => {
const list = doc1.putObject('_root', 'list', [])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'key1', value: 1, datatype: 'int', conflict: false },
{ action: 'put', obj: '_root', key: 'key1', value: 2, datatype: 'int', conflict: false },
{ action: 'put', obj: '_root', key: 'key2', value: 3, datatype: 'int', conflict: false },
{ action: 'put', obj: '_root', key: 'map', value: map, datatype: 'map', conflict: false },
{ action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
{ action: 'put', path: ['key1'], value: 1, conflict: false },
{ action: 'put', path: ['key1'], value: 2, conflict: false },
{ action: 'put', path: ['key2'], value: 3, conflict: false },
{ action: 'put', path: ['map'], value: {}, conflict: false },
{ action: 'put', path: ['list'], value: [], conflict: false },
])
doc1.free()
})
@ -914,12 +902,12 @@ describe('Automerge', () => {
const list2 = doc1.insertObject(list, 2, [])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
{ action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
{ action: 'insert', obj: list, key: 0, value: 2, datatype: 'int' },
{ action: 'insert', obj: list, key: 2, value: 3, datatype: 'int' },
{ action: 'insert', obj: list, key: 2, value: map, datatype: 'map' },
{ action: 'insert', obj: list, key: 2, value: list2, datatype: 'list' },
{ action: 'put', path: ['list'], value: [], conflict: false },
{ action: 'splice', path: ['list', 0], values: [1] },
{ action: 'splice', path: ['list', 0], values: [2] },
{ action: 'splice', path: ['list', 2], values: [3] },
{ action: 'splice', path: ['list', 2], values: [{}] },
{ action: 'splice', path: ['list', 2], values: [[]] },
])
doc1.free()
})
@ -933,10 +921,8 @@ describe('Automerge', () => {
const list2 = doc1.pushObject(list, [])
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
{ action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
{ action: 'insert', obj: list, key: 1, value: map, datatype: 'map' },
{ action: 'insert', obj: list, key: 2, value: list2, datatype: 'list' },
{ action: 'put', path: ['list'], value: [], conflict: false },
{ action: 'splice', path: ['list',0], values: [1,{},[]] },
])
doc1.free()
})
@ -949,13 +935,10 @@ describe('Automerge', () => {
doc1.splice(list, 1, 2)
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
{ action: 'insert', obj: list, key: 0, value: 1, datatype: 'int' },
{ action: 'insert', obj: list, key: 1, value: 2, datatype: 'int' },
{ action: 'insert', obj: list, key: 2, value: 3, datatype: 'int' },
{ action: 'insert', obj: list, key: 3, value: 4, datatype: 'int' },
{ action: 'delete', obj: list, key: 1 },
{ action: 'delete', obj: list, key: 1 },
{ action: 'put', path: ['list'], value: [], conflict: false },
{ action: 'splice', path: ['list',0], values: [1,2,3,4] },
{ action: 'del', path: ['list',1] },
{ action: 'del', path: ['list',1] },
])
doc1.free()
})
@ -967,8 +950,8 @@ describe('Automerge', () => {
doc1.increment('_root', 'counter', 4)
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'counter', value: 2, datatype: 'counter', conflict: false },
{ action: 'increment', obj: '_root', key: 'counter', value: 4 },
{ action: 'put', path: ['counter'], value: 2, conflict: false },
{ action: 'inc', path: ['counter'], value: 4 },
])
doc1.free()
})
@ -982,10 +965,10 @@ describe('Automerge', () => {
doc1.delete('_root', 'key1')
doc1.delete('_root', 'key2')
assert.deepEqual(doc1.popPatches(), [
{ action: 'put', obj: '_root', key: 'key1', value: 1, datatype: 'int', conflict: false },
{ action: 'put', obj: '_root', key: 'key2', value: 2, datatype: 'int', conflict: false },
{ action: 'delete', obj: '_root', key: 'key1' },
{ action: 'delete', obj: '_root', key: 'key2' },
{ action: 'put', path: ['key1'], value: 1, conflict: false },
{ action: 'put', path: ['key2'], value: 2, conflict: false },
{ action: 'del', path: ['key1'], },
{ action: 'del', path: ['key2'], },
])
doc1.free()
})
@ -999,8 +982,8 @@ describe('Automerge', () => {
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.getWithType('_root', 'starlings'), ['counter', 3])
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'starlings', value: 2, datatype: 'counter', conflict: false },
{ action: 'increment', obj: '_root', key: 'starlings', value: 1 }
{ action: 'put', path: ['starlings'], value: 2, conflict: false },
{ action: 'inc', path: ['starlings'], value: 1 }
])
doc1.free(); doc2.free()
})
@ -1018,10 +1001,10 @@ describe('Automerge', () => {
doc2.loadIncremental(doc1.saveIncremental())
assert.deepEqual(doc2.popPatches(), [
{ action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false },
{ action: 'insert', obj: list, key: 0, value: 1, datatype: 'counter' },
{ action: 'increment', obj: list, key: 0, value: 2 },
{ action: 'increment', obj: list, key: 0, value: -5 },
{ action: 'put', path: ['list'], value: [], conflict: false },
{ action: 'splice', path: ['list',0], values: [1] },
{ action: 'inc', path: ['list',0], value: 2 },
{ action: 'inc', path: ['list',0], value: -5 },
])
doc1.free(); doc2.free()
})

View file

@ -9,19 +9,19 @@ use automerge::ROOT;
fn main() {
let mut doc = Automerge::new();
let mut observer = VecOpObserver::default();
// a simple scalar change in the root object
doc.transact_with::<_, _, AutomergeError, _, _>(
|_result| CommitOptions::default().with_op_observer(&mut observer),
|tx| {
tx.put(ROOT, "hello", "world").unwrap();
Ok(())
},
)
.unwrap();
get_changes(&doc, observer.take_patches());
let mut result = doc
.transact_with::<_, _, AutomergeError, _, VecOpObserver>(
|_result| CommitOptions::default(),
|tx| {
tx.put(ROOT, "hello", "world").unwrap();
Ok(())
},
)
.unwrap();
get_changes(&doc, result.op_observer.take_patches());
let mut tx = doc.transaction();
let mut tx = doc.transaction_with_observer(VecOpObserver::default());
let map = tx
.put_object(ROOT, "my new map", automerge::ObjType::Map)
.unwrap();
@ -36,28 +36,28 @@ fn main() {
tx.insert(&list, 1, "woo").unwrap();
let m = tx.insert_object(&list, 2, automerge::ObjType::Map).unwrap();
tx.put(&m, "hi", 2).unwrap();
let _heads3 = tx.commit_with(CommitOptions::default().with_op_observer(&mut observer));
get_changes(&doc, observer.take_patches());
let patches = tx.op_observer.take_patches();
let _heads3 = tx.commit_with(CommitOptions::default());
get_changes(&doc, patches);
}
fn get_changes(doc: &Automerge, patches: Vec<Patch>) {
for patch in patches {
match patch {
Patch::Put {
obj,
key,
value,
conflict: _,
obj, prop, value, ..
} => {
println!(
"put {:?} at {:?} in obj {:?}, object path {:?}",
value,
key,
prop,
obj,
doc.path_to_object(&obj)
)
}
Patch::Insert { obj, index, value } => {
Patch::Insert {
obj, index, value, ..
} => {
println!(
"insert {:?} at {:?} in obj {:?}, object path {:?}",
value,
@ -66,18 +66,20 @@ fn get_changes(doc: &Automerge, patches: Vec<Patch>) {
doc.path_to_object(&obj)
)
}
Patch::Increment { obj, key, value } => {
Patch::Increment {
obj, prop, value, ..
} => {
println!(
"increment {:?} in obj {:?} by {:?}, object path {:?}",
key,
prop,
obj,
value,
doc.path_to_object(&obj)
)
}
Patch::Delete { obj, key } => println!(
Patch::Delete { obj, prop, .. } => println!(
"delete {:?} in obj {:?}, object path {:?}",
key,
prop,
obj,
doc.path_to_object(&obj)
),

View file

@ -4,8 +4,7 @@ use crate::exid::ExId;
use crate::op_observer::OpObserver;
use crate::transaction::{CommitOptions, Transactable};
use crate::{
sync, ApplyOptions, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType,
Parents, ScalarValue,
sync, Keys, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType, Parents, ScalarValue,
};
use crate::{
transaction::TransactionInner, ActorId, Automerge, AutomergeError, Change, ChangeHash, Prop,
@ -14,22 +13,46 @@ use crate::{
/// An automerge document that automatically manages transactions.
#[derive(Debug, Clone)]
pub struct AutoCommit {
pub struct AutoCommitWithObs<Obs: OpObserver> {
doc: Automerge,
transaction: Option<TransactionInner>,
transaction: Option<(Obs, TransactionInner)>,
op_observer: Obs,
}
impl Default for AutoCommit {
pub type AutoCommit = AutoCommitWithObs<()>;
impl<O: OpObserver> Default for AutoCommitWithObs<O> {
fn default() -> Self {
Self::new()
let op_observer = O::default();
AutoCommitWithObs {
doc: Automerge::new(),
transaction: None,
op_observer,
}
}
}
impl AutoCommit {
pub fn new() -> Self {
Self {
pub fn new() -> AutoCommit {
AutoCommitWithObs {
doc: Automerge::new(),
transaction: None,
op_observer: (),
}
}
}
impl<Obs: OpObserver> AutoCommitWithObs<Obs> {
pub fn observer(&mut self) -> &mut Obs {
self.ensure_transaction_closed();
&mut self.op_observer
}
pub fn with_observer<Obs2: OpObserver>(self, op_observer: Obs2) -> AutoCommitWithObs<Obs2> {
AutoCommitWithObs {
doc: self.doc,
transaction: self.transaction.map(|(_, t)| (op_observer.branch(), t)),
op_observer,
}
}
@ -58,7 +81,7 @@ impl AutoCommit {
fn ensure_transaction_open(&mut self) {
if self.transaction.is_none() {
self.transaction = Some(self.doc.transaction_inner());
self.transaction = Some((self.op_observer.branch(), self.doc.transaction_inner()));
}
}
@ -67,6 +90,7 @@ impl AutoCommit {
Self {
doc: self.doc.fork(),
transaction: self.transaction.clone(),
op_observer: self.op_observer.clone(),
}
}
@ -75,46 +99,35 @@ impl AutoCommit {
Ok(Self {
doc: self.doc.fork_at(heads)?,
transaction: self.transaction.clone(),
op_observer: self.op_observer.clone(),
})
}
fn ensure_transaction_closed(&mut self) {
if let Some(tx) = self.transaction.take() {
tx.commit::<()>(&mut self.doc, None, None, None);
if let Some((current, tx)) = self.transaction.take() {
self.op_observer.merge(&current);
tx.commit(&mut self.doc, None, None);
}
}
pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
// passing a () observer here has performance implications on all loads
// if we want an autocommit::load() method that can be observered we need to make a new method
// fn observed_load() ?
let doc = Automerge::load(data)?;
let op_observer = Obs::default();
Ok(Self {
doc,
transaction: None,
})
}
pub fn load_with<Obs: OpObserver>(
data: &[u8],
options: ApplyOptions<'_, Obs>,
) -> Result<Self, AutomergeError> {
let doc = Automerge::load_with(data, options)?;
Ok(Self {
doc,
transaction: None,
op_observer,
})
}
pub fn load_incremental(&mut self, data: &[u8]) -> Result<usize, AutomergeError> {
self.ensure_transaction_closed();
self.doc.load_incremental(data)
}
pub fn load_incremental_with<'a, Obs: OpObserver>(
&mut self,
data: &[u8],
options: ApplyOptions<'a, Obs>,
) -> Result<usize, AutomergeError> {
self.ensure_transaction_closed();
self.doc.load_incremental_with(data, options)
// TODO - would be nice to pass None here instead of &mut ()
self.doc
.load_incremental_with(data, Some(&mut self.op_observer))
}
pub fn apply_changes(
@ -122,34 +135,19 @@ impl AutoCommit {
changes: impl IntoIterator<Item = Change>,
) -> Result<(), AutomergeError> {
self.ensure_transaction_closed();
self.doc.apply_changes(changes)
}
pub fn apply_changes_with<I: IntoIterator<Item = Change>, Obs: OpObserver>(
&mut self,
changes: I,
options: ApplyOptions<'_, Obs>,
) -> Result<(), AutomergeError> {
self.ensure_transaction_closed();
self.doc.apply_changes_with(changes, options)
self.doc
.apply_changes_with(changes, Some(&mut self.op_observer))
}
/// Takes all the changes in `other` which are not in `self` and applies them
pub fn merge(&mut self, other: &mut Self) -> Result<Vec<ChangeHash>, AutomergeError> {
self.ensure_transaction_closed();
other.ensure_transaction_closed();
self.doc.merge(&mut other.doc)
}
/// Takes all the changes in `other` which are not in `self` and applies them
pub fn merge_with<'a, Obs: OpObserver>(
pub fn merge<Obs2: OpObserver>(
&mut self,
other: &mut Self,
options: ApplyOptions<'a, Obs>,
other: &mut AutoCommitWithObs<Obs2>,
) -> Result<Vec<ChangeHash>, AutomergeError> {
self.ensure_transaction_closed();
other.ensure_transaction_closed();
self.doc.merge_with(&mut other.doc, options)
self.doc
.merge_with(&mut other.doc, Some(&mut self.op_observer))
}
pub fn save(&mut self) -> Vec<u8> {
@ -220,17 +218,6 @@ impl AutoCommit {
self.doc.receive_sync_message(sync_state, message)
}
pub fn receive_sync_message_with<'a, Obs: OpObserver>(
&mut self,
sync_state: &mut sync::State,
message: sync::Message,
options: ApplyOptions<'a, Obs>,
) -> Result<(), AutomergeError> {
self.ensure_transaction_closed();
self.doc
.receive_sync_message_with(sync_state, message, options)
}
/// Return a graphviz representation of the opset.
///
/// # Arguments
@ -251,7 +238,7 @@ impl AutoCommit {
}
pub fn commit(&mut self) -> ChangeHash {
self.commit_with::<()>(CommitOptions::default())
self.commit_with(CommitOptions::default())
}
/// Commit the current operations with some options.
@ -267,33 +254,29 @@ impl AutoCommit {
/// doc.put_object(&ROOT, "todos", ObjType::List).unwrap();
/// let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as
/// i64;
/// doc.commit_with::<()>(CommitOptions::default().with_message("Create todos list").with_time(now));
/// doc.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now));
/// ```
pub fn commit_with<Obs: OpObserver>(&mut self, options: CommitOptions<'_, Obs>) -> ChangeHash {
pub fn commit_with(&mut self, options: CommitOptions) -> ChangeHash {
// ensure that even no changes triggers a change
self.ensure_transaction_open();
let tx = self.transaction.take().unwrap();
tx.commit(
&mut self.doc,
options.message,
options.time,
options.op_observer,
)
let (current, tx) = self.transaction.take().unwrap();
self.op_observer.merge(&current);
tx.commit(&mut self.doc, options.message, options.time)
}
pub fn rollback(&mut self) -> usize {
self.transaction
.take()
.map(|tx| tx.rollback(&mut self.doc))
.map(|(_, tx)| tx.rollback(&mut self.doc))
.unwrap_or(0)
}
}
impl Transactable for AutoCommit {
impl<Obs: OpObserver> Transactable for AutoCommitWithObs<Obs> {
fn pending_ops(&self) -> usize {
self.transaction
.as_ref()
.map(|t| t.pending_ops())
.map(|(_, t)| t.pending_ops())
.unwrap_or(0)
}
@ -389,8 +372,8 @@ impl Transactable for AutoCommit {
value: V,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.put(&mut self.doc, obj.as_ref(), prop, value)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.put(&mut self.doc, current, obj.as_ref(), prop, value)
}
fn put_object<O: AsRef<ExId>, P: Into<Prop>>(
@ -400,8 +383,8 @@ impl Transactable for AutoCommit {
value: ObjType,
) -> Result<ExId, AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.put_object(&mut self.doc, obj.as_ref(), prop, value)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.put_object(&mut self.doc, current, obj.as_ref(), prop, value)
}
fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
@ -411,8 +394,8 @@ impl Transactable for AutoCommit {
value: V,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.insert(&mut self.doc, obj.as_ref(), index, value)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.insert(&mut self.doc, current, obj.as_ref(), index, value)
}
fn insert_object<O: AsRef<ExId>>(
@ -422,8 +405,8 @@ impl Transactable for AutoCommit {
value: ObjType,
) -> Result<ExId, AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.insert_object(&mut self.doc, obj.as_ref(), index, value)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.insert_object(&mut self.doc, current, obj.as_ref(), index, value)
}
fn increment<O: AsRef<ExId>, P: Into<Prop>>(
@ -433,8 +416,8 @@ impl Transactable for AutoCommit {
value: i64,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.increment(&mut self.doc, obj.as_ref(), prop, value)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.increment(&mut self.doc, current, obj.as_ref(), prop, value)
}
fn delete<O: AsRef<ExId>, P: Into<Prop>>(
@ -443,8 +426,8 @@ impl Transactable for AutoCommit {
prop: P,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.delete(&mut self.doc, obj.as_ref(), prop)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.delete(&mut self.doc, current, obj.as_ref(), prop)
}
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
@ -457,8 +440,8 @@ impl Transactable for AutoCommit {
vals: V,
) -> Result<(), AutomergeError> {
self.ensure_transaction_open();
let tx = self.transaction.as_mut().unwrap();
tx.splice(&mut self.doc, obj.as_ref(), pos, del, vals)
let (current, tx) = self.transaction.as_mut().unwrap();
tx.splice(&mut self.doc, current, obj.as_ref(), pos, del, vals)
}
fn text<O: AsRef<ExId>>(&self, obj: O) -> Result<String, AutomergeError> {

View file

@ -19,8 +19,8 @@ use crate::types::{
ScalarValue, Value,
};
use crate::{
query, ApplyOptions, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange,
MapRangeAt, ObjType, Prop, Values,
query, AutomergeError, Change, KeysAt, ListRange, ListRangeAt, MapRange, MapRangeAt, ObjType,
Prop, Values,
};
use serde::Serialize;
@ -111,10 +111,22 @@ impl Automerge {
}
/// Start a transaction.
pub fn transaction(&mut self) -> Transaction<'_> {
pub fn transaction(&mut self) -> Transaction<'_, ()> {
Transaction {
inner: Some(self.transaction_inner()),
doc: self,
op_observer: (),
}
}
pub fn transaction_with_observer<Obs: OpObserver>(
&mut self,
op_observer: Obs,
) -> Transaction<'_, Obs> {
Transaction {
inner: Some(self.transaction_inner()),
doc: self,
op_observer,
}
}
@ -143,15 +155,16 @@ impl Automerge {
/// Run a transaction on this document in a closure, automatically handling commit or rollback
/// afterwards.
pub fn transact<F, O, E>(&mut self, f: F) -> transaction::Result<O, E>
pub fn transact<F, O, E>(&mut self, f: F) -> transaction::Result<O, (), E>
where
F: FnOnce(&mut Transaction<'_>) -> Result<O, E>,
F: FnOnce(&mut Transaction<'_, ()>) -> Result<O, E>,
{
let mut tx = self.transaction();
let result = f(&mut tx);
match result {
Ok(result) => Ok(Success {
result,
op_observer: (),
hash: tx.commit(),
}),
Err(error) => Err(Failure {
@ -162,19 +175,25 @@ impl Automerge {
}
/// Like [`Self::transact`] but with a function for generating the commit options.
pub fn transact_with<'a, F, O, E, C, Obs>(&mut self, c: C, f: F) -> transaction::Result<O, E>
pub fn transact_with<F, O, E, C, Obs>(&mut self, c: C, f: F) -> transaction::Result<O, Obs, E>
where
F: FnOnce(&mut Transaction<'_>) -> Result<O, E>,
C: FnOnce(&O) -> CommitOptions<'a, Obs>,
Obs: 'a + OpObserver,
F: FnOnce(&mut Transaction<'_, Obs>) -> Result<O, E>,
C: FnOnce(&O) -> CommitOptions,
Obs: OpObserver,
{
let mut tx = self.transaction();
let mut op_observer = Obs::default();
let mut tx = self.transaction_with_observer(Default::default());
let result = f(&mut tx);
match result {
Ok(result) => {
let commit_options = c(&result);
std::mem::swap(&mut op_observer, &mut tx.op_observer);
let hash = tx.commit_with(commit_options);
Ok(Success { result, hash })
Ok(Success {
result,
hash,
op_observer,
})
}
Err(error) => Err(Failure {
error,
@ -220,17 +239,6 @@ impl Automerge {
// PropAt::()
// NthAt::()
/// Get the object id of the object that contains this object and the prop that this object is
/// at in that object.
pub(crate) fn parent_object(&self, obj: ObjId) -> Option<(ObjId, Key)> {
if obj == ObjId::root() {
// root has no parent
None
} else {
self.ops.parent_object(&obj)
}
}
/// Get the parents of an object in the document tree.
///
/// ### Errors
@ -244,10 +252,7 @@ impl Automerge {
/// value.
pub fn parents<O: AsRef<ExId>>(&self, obj: O) -> Result<Parents<'_>, AutomergeError> {
let obj_id = self.exid_to_obj(obj.as_ref())?;
Ok(Parents {
obj: obj_id,
doc: self,
})
Ok(self.ops.parents(obj_id))
}
pub fn path_to_object<O: AsRef<ExId>>(
@ -259,21 +264,6 @@ impl Automerge {
Ok(path)
}
/// Export a key to a prop.
pub(crate) fn export_key(&self, obj: ObjId, key: Key) -> Prop {
match key {
Key::Map(m) => Prop::Map(self.ops.m.props.get(m).into()),
Key::Seq(opid) => {
let i = self
.ops
.search(&obj, query::ElemIdPos::new(opid))
.index()
.unwrap();
Prop::Seq(i)
}
}
}
/// Get the keys of the object `obj`.
///
/// For a map this returns the keys of the map.
@ -587,14 +577,14 @@ impl Automerge {
/// Load a document.
pub fn load(data: &[u8]) -> Result<Self, AutomergeError> {
Self::load_with::<()>(data, ApplyOptions::default())
Self::load_with::<()>(data, None)
}
/// Load a document.
#[tracing::instrument(skip(data, options), err)]
#[tracing::instrument(skip(data, observer), err)]
pub fn load_with<Obs: OpObserver>(
data: &[u8],
mut options: ApplyOptions<'_, Obs>,
mut observer: Option<&mut Obs>,
) -> Result<Self, AutomergeError> {
if data.is_empty() {
tracing::trace!("no data, initializing empty document");
@ -606,7 +596,6 @@ impl Automerge {
if !first_chunk.checksum_valid() {
return Err(load::Error::BadChecksum.into());
}
let observer = &mut options.op_observer;
let mut am = match first_chunk {
storage::Chunk::Document(d) => {
@ -616,7 +605,7 @@ impl Automerge {
result: op_set,
changes,
heads,
} = match observer {
} = match &mut observer {
Some(o) => storage::load::reconstruct_document(&d, OpSet::observed_builder(*o)),
None => storage::load::reconstruct_document(&d, OpSet::builder()),
}
@ -651,7 +640,7 @@ impl Automerge {
let change = Change::new_from_unverified(stored_change.into_owned(), None)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, observer);
am.apply_change(change, &mut observer);
am
}
storage::Chunk::CompressedChange(stored_change, compressed) => {
@ -662,7 +651,7 @@ impl Automerge {
)
.map_err(|e| load::Error::InvalidChangeColumns(Box::new(e)))?;
let mut am = Self::new();
am.apply_change(change, observer);
am.apply_change(change, &mut observer);
am
}
};
@ -670,7 +659,7 @@ impl Automerge {
match load::load_changes(remaining.reset()) {
load::LoadedChanges::Complete(c) => {
for change in c {
am.apply_change(change, observer);
am.apply_change(change, &mut observer);
}
}
load::LoadedChanges::Partial { error, .. } => return Err(error.into()),
@ -680,14 +669,14 @@ impl Automerge {
/// Load an incremental save of a document.
pub fn load_incremental(&mut self, data: &[u8]) -> Result<usize, AutomergeError> {
self.load_incremental_with::<()>(data, ApplyOptions::default())
self.load_incremental_with::<()>(data, None)
}
/// Load an incremental save of a document.
pub fn load_incremental_with<Obs: OpObserver>(
&mut self,
data: &[u8],
options: ApplyOptions<'_, Obs>,
op_observer: Option<&mut Obs>,
) -> Result<usize, AutomergeError> {
let changes = match load::load_changes(storage::parse::Input::new(data)) {
load::LoadedChanges::Complete(c) => c,
@ -697,7 +686,7 @@ impl Automerge {
}
};
let start = self.ops.len();
self.apply_changes_with(changes, options)?;
self.apply_changes_with(changes, op_observer)?;
let delta = self.ops.len() - start;
Ok(delta)
}
@ -717,14 +706,14 @@ impl Automerge {
&mut self,
changes: impl IntoIterator<Item = Change>,
) -> Result<(), AutomergeError> {
self.apply_changes_with::<_, ()>(changes, ApplyOptions::default())
self.apply_changes_with::<_, ()>(changes, None)
}
/// Apply changes to this document.
pub fn apply_changes_with<I: IntoIterator<Item = Change>, Obs: OpObserver>(
&mut self,
changes: I,
mut options: ApplyOptions<'_, Obs>,
mut op_observer: Option<&mut Obs>,
) -> Result<(), AutomergeError> {
for c in changes {
if !self.history_index.contains_key(&c.hash()) {
@ -735,7 +724,7 @@ impl Automerge {
));
}
if self.is_causally_ready(&c) {
self.apply_change(c, &mut options.op_observer);
self.apply_change(c, &mut op_observer);
} else {
self.queue.push(c);
}
@ -743,7 +732,7 @@ impl Automerge {
}
while let Some(c) = self.pop_next_causally_ready_change() {
if !self.history_index.contains_key(&c.hash()) {
self.apply_change(c, &mut options.op_observer);
self.apply_change(c, &mut op_observer);
}
}
Ok(())
@ -831,14 +820,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<Vec<ChangeHash>, AutomergeError> {
self.merge_with::<()>(other, ApplyOptions::default())
self.merge_with::<()>(other, None)
}
/// Takes all the changes in `other` which are not in `self` and applies them
pub fn merge_with<'a, Obs: OpObserver>(
pub fn merge_with<Obs: OpObserver>(
&mut self,
other: &mut Self,
options: ApplyOptions<'a, Obs>,
op_observer: Option<&mut Obs>,
) -> Result<Vec<ChangeHash>, AutomergeError> {
// TODO: Make this fallible and figure out how to do this transactionally
let changes = self
@ -847,7 +836,7 @@ impl Automerge {
.cloned()
.collect::<Vec<_>>();
tracing::trace!(changes=?changes.iter().map(|c| c.hash()).collect::<Vec<_>>(), "merging new changes");
self.apply_changes_with(changes, options)?;
self.apply_changes_with(changes, op_observer)?;
Ok(self.get_heads())
}

View file

@ -1437,19 +1437,15 @@ fn observe_counter_change_application_overwrite() {
doc1.increment(ROOT, "counter", 5).unwrap();
doc1.commit();
let mut observer = VecOpObserver::default();
let mut doc3 = doc1.clone();
doc3.merge_with(
&mut doc2,
ApplyOptions::default().with_op_observer(&mut observer),
)
.unwrap();
let mut doc3 = doc1.fork().with_observer(VecOpObserver::default());
doc3.merge(&mut doc2).unwrap();
assert_eq!(
observer.take_patches(),
doc3.observer().take_patches(),
vec![Patch::Put {
obj: ExId::Root,
key: Prop::Map("counter".into()),
path: vec![],
prop: Prop::Map("counter".into()),
value: (
ScalarValue::Str("mystring".into()).into(),
ExId::Id(2, doc2.get_actor().clone(), 1)
@ -1458,16 +1454,11 @@ fn observe_counter_change_application_overwrite() {
}]
);
let mut observer = VecOpObserver::default();
let mut doc4 = doc2.clone();
doc4.merge_with(
&mut doc1,
ApplyOptions::default().with_op_observer(&mut observer),
)
.unwrap();
let mut doc4 = doc2.clone().with_observer(VecOpObserver::default());
doc4.merge(&mut doc1).unwrap();
// no patches as the increments operate on an invisible counter
assert_eq!(observer.take_patches(), vec![]);
assert_eq!(doc4.observer().take_patches(), vec![]);
}
#[test]
@ -1478,20 +1469,15 @@ fn observe_counter_change_application() {
doc.increment(ROOT, "counter", 5).unwrap();
let changes = doc.get_changes(&[]).unwrap().into_iter().cloned();
let mut new_doc = AutoCommit::new();
let mut observer = VecOpObserver::default();
new_doc
.apply_changes_with(
changes,
ApplyOptions::default().with_op_observer(&mut observer),
)
.unwrap();
let mut new_doc = AutoCommit::new().with_observer(VecOpObserver::default());
new_doc.apply_changes(changes).unwrap();
assert_eq!(
observer.take_patches(),
new_doc.observer().take_patches(),
vec![
Patch::Put {
obj: ExId::Root,
key: Prop::Map("counter".into()),
path: vec![],
prop: Prop::Map("counter".into()),
value: (
ScalarValue::counter(1).into(),
ExId::Id(1, doc.get_actor().clone(), 0)
@ -1500,12 +1486,14 @@ fn observe_counter_change_application() {
},
Patch::Increment {
obj: ExId::Root,
key: Prop::Map("counter".into()),
path: vec![],
prop: Prop::Map("counter".into()),
value: (2, ExId::Id(2, doc.get_actor().clone(), 0)),
},
Patch::Increment {
obj: ExId::Root,
key: Prop::Map("counter".into()),
path: vec![],
prop: Prop::Map("counter".into()),
value: (5, ExId::Id(3, doc.get_actor().clone(), 0)),
}
]
@ -1514,7 +1502,7 @@ fn observe_counter_change_application() {
#[test]
fn get_changes_heads_empty() {
let mut doc = AutoCommit::new();
let mut doc = AutoCommit::default();
doc.put(ROOT, "key1", 1).unwrap();
doc.commit();
doc.put(ROOT, "key2", 1).unwrap();

View file

@ -75,7 +75,6 @@ mod map_range_at;
mod op_observer;
mod op_set;
mod op_tree;
mod options;
mod parents;
mod query;
mod storage;
@ -88,7 +87,7 @@ mod values;
mod visualisation;
pub use crate::automerge::Automerge;
pub use autocommit::AutoCommit;
pub use autocommit::{AutoCommit, AutoCommitWithObs};
pub use autoserde::AutoSerde;
pub use change::{Change, LoadError as LoadChangeError};
pub use error::AutomergeError;
@ -105,7 +104,6 @@ pub use map_range_at::MapRangeAt;
pub use op_observer::OpObserver;
pub use op_observer::Patch;
pub use op_observer::VecOpObserver;
pub use options::ApplyOptions;
pub use parents::Parents;
pub use types::{ActorId, ChangeHash, ObjType, OpType, Prop};
pub use value::{ScalarValue, Value};

View file

@ -1,50 +1,113 @@
use crate::exid::ExId;
use crate::Parents;
use crate::Prop;
use crate::Value;
/// An observer of operations applied to the document.
pub trait OpObserver {
pub trait OpObserver: Default + Clone {
/// A new value has been inserted into the given object.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been inserted into.
/// - `index`: the index the new value has been inserted at.
/// - `tagged_value`: the value that has been inserted and the id of the operation that did the
/// insert.
fn insert(&mut self, objid: ExId, index: usize, tagged_value: (Value<'_>, ExId));
fn insert(
&mut self,
parents: Parents<'_>,
objid: ExId,
index: usize,
tagged_value: (Value<'_>, ExId),
);
/// A new value has been put into the given object.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been put into.
/// - `key`: the key that the value as been put at.
/// - `prop`: the prop that the value as been put at.
/// - `tagged_value`: the value that has been put into the object and the id of the operation
/// that did the put.
/// - `conflict`: whether this put conflicts with other operations.
fn put(&mut self, objid: ExId, key: Prop, tagged_value: (Value<'_>, ExId), conflict: bool);
fn put(
&mut self,
parents: Parents<'_>,
objid: ExId,
prop: Prop,
tagged_value: (Value<'_>, ExId),
conflict: bool,
);
/// A counter has been incremented.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that contains the counter.
/// - `key`: they key that the chounter is at.
/// - `prop`: they prop that the chounter is at.
/// - `tagged_value`: the amount the counter has been incremented by, and the the id of the
/// increment operation.
fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId));
fn increment(
&mut self,
parents: Parents<'_>,
objid: ExId,
prop: Prop,
tagged_value: (i64, ExId),
);
/// A value has beeen deleted.
///
/// - `parents`: A parents iterator that can be used to collect path information
/// - `objid`: the object that has been deleted in.
/// - `key`: the key of the value that has been deleted.
fn delete(&mut self, objid: ExId, key: Prop);
/// - `prop`: the prop of the value that has been deleted.
fn delete(&mut self, parents: Parents<'_>, objid: ExId, prop: Prop);
/// Branch of a new op_observer later to be merged
///
/// Called by AutoCommit when creating a new transaction. Observer branch
/// will be merged on `commit()` or thrown away on `rollback()`
///
fn branch(&self) -> Self {
Self::default()
}
/// Merge observed information from a transaction.
///
/// Called by AutoCommit on `commit()`
///
/// - `other`: Another Op Observer of the same type
fn merge(&mut self, other: &Self);
}
impl OpObserver for () {
fn insert(&mut self, _objid: ExId, _index: usize, _tagged_value: (Value<'_>, ExId)) {}
fn put(&mut self, _objid: ExId, _key: Prop, _tagged_value: (Value<'_>, ExId), _conflict: bool) {
fn insert(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_index: usize,
_tagged_value: (Value<'_>, ExId),
) {
}
fn increment(&mut self, _objid: ExId, _key: Prop, _tagged_value: (i64, ExId)) {}
fn put(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_prop: Prop,
_tagged_value: (Value<'_>, ExId),
_conflict: bool,
) {
}
fn delete(&mut self, _objid: ExId, _key: Prop) {}
fn increment(
&mut self,
_parents: Parents<'_>,
_objid: ExId,
_prop: Prop,
_tagged_value: (i64, ExId),
) {
}
fn delete(&mut self, _parents: Parents<'_>, _objid: ExId, _prop: Prop) {}
fn merge(&mut self, _other: &Self) {}
}
/// Capture operations into a [`Vec`] and store them as patches.
@ -62,45 +125,77 @@ impl VecOpObserver {
}
impl OpObserver for VecOpObserver {
fn insert(&mut self, obj_id: ExId, index: usize, (value, id): (Value<'_>, ExId)) {
fn insert(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
index: usize,
(value, id): (Value<'_>, ExId),
) {
let path = parents.path();
self.patches.push(Patch::Insert {
obj: obj_id,
obj,
path,
index,
value: (value.into_owned(), id),
});
}
fn put(&mut self, objid: ExId, key: Prop, (value, id): (Value<'_>, ExId), conflict: bool) {
fn put(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
prop: Prop,
(value, id): (Value<'_>, ExId),
conflict: bool,
) {
let path = parents.path();
self.patches.push(Patch::Put {
obj: objid,
key,
obj,
path,
prop,
value: (value.into_owned(), id),
conflict,
});
}
fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId)) {
fn increment(
&mut self,
mut parents: Parents<'_>,
obj: ExId,
prop: Prop,
tagged_value: (i64, ExId),
) {
let path = parents.path();
self.patches.push(Patch::Increment {
obj: objid,
key,
obj,
path,
prop,
value: tagged_value,
});
}
fn delete(&mut self, objid: ExId, key: Prop) {
self.patches.push(Patch::Delete { obj: objid, key })
fn delete(&mut self, mut parents: Parents<'_>, obj: ExId, prop: Prop) {
let path = parents.path();
self.patches.push(Patch::Delete { obj, path, prop })
}
fn merge(&mut self, other: &Self) {
self.patches.extend_from_slice(other.patches.as_slice())
}
}
/// A notification to the application that something has changed in a document.
#[derive(Debug, Clone, PartialEq)]
pub enum Patch {
/// Associating a new value with a key in a map, or an existing list element
/// Associating a new value with a prop in a map, or an existing list element
Put {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was put into.
obj: ExId,
/// The key that the new value was put at.
key: Prop,
/// The prop that the new value was put at.
prop: Prop,
/// The value that was put, and the id of the operation that put it there.
value: (Value<'static>, ExId),
/// Whether this put conflicts with another.
@ -108,6 +203,8 @@ pub enum Patch {
},
/// Inserting a new element into a list/text
Insert {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was inserted into.
obj: ExId,
/// The index that the new value was inserted at.
@ -117,19 +214,23 @@ pub enum Patch {
},
/// Incrementing a counter.
Increment {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was incremented in.
obj: ExId,
/// The key that was incremented.
key: Prop,
/// The prop that was incremented.
prop: Prop,
/// The amount that the counter was incremented by, and the id of the operation that
/// did the increment.
value: (i64, ExId),
},
/// Deleting an element from a list/text
Delete {
/// path to the object
path: Vec<(ExId, Prop)>,
/// The object that was deleted from.
obj: ExId,
/// The key that was deleted.
key: Prop,
/// The prop that was deleted.
prop: Prop,
},
}

View file

@ -2,8 +2,9 @@ use crate::clock::Clock;
use crate::exid::ExId;
use crate::indexed_cache::IndexedCache;
use crate::op_tree::{self, OpTree};
use crate::parents::Parents;
use crate::query::{self, OpIdSearch, TreeQuery};
use crate::types::{self, ActorId, Key, ObjId, Op, OpId, OpIds, OpType};
use crate::types::{self, ActorId, Key, ObjId, Op, OpId, OpIds, OpType, Prop};
use crate::{ObjType, OpObserver};
use fxhash::FxBuildHasher;
use std::borrow::Borrow;
@ -68,12 +69,29 @@ impl OpSetInternal {
}
}
pub(crate) fn parents(&self, obj: ObjId) -> Parents<'_> {
Parents { obj, ops: self }
}
pub(crate) fn parent_object(&self, obj: &ObjId) -> Option<(ObjId, Key)> {
let parent = self.trees.get(obj)?.parent?;
let key = self.search(&parent, OpIdSearch::new(obj.0)).key().unwrap();
Some((parent, key))
}
pub(crate) fn export_key(&self, obj: ObjId, key: Key) -> Prop {
match key {
Key::Map(m) => Prop::Map(self.m.props.get(m).into()),
Key::Seq(opid) => {
let i = self
.search(&obj, query::ElemIdPos::new(opid))
.index()
.unwrap();
Prop::Seq(i)
}
}
}
pub(crate) fn keys(&self, obj: ObjId) -> Option<query::Keys<'_>> {
if let Some(tree) = self.trees.get(&obj) {
tree.internal.keys()
@ -245,6 +263,8 @@ impl OpSetInternal {
} = q;
let ex_obj = self.id_to_exid(obj.0);
let parents = self.parents(*obj);
let key = match op.key {
Key::Map(index) => self.m.props[index].clone().into(),
Key::Seq(_) => seen.into(),
@ -252,21 +272,21 @@ impl OpSetInternal {
if op.insert {
let value = (op.value(), self.id_to_exid(op.id));
observer.insert(ex_obj, seen, value);
observer.insert(parents, ex_obj, seen, value);
} else if op.is_delete() {
if let Some(winner) = &values.last() {
let value = (winner.value(), self.id_to_exid(winner.id));
let conflict = values.len() > 1;
observer.put(ex_obj, key, value, conflict);
observer.put(parents, ex_obj, key, value, conflict);
} else {
observer.delete(ex_obj, key);
observer.delete(parents, ex_obj, key);
}
} else if let Some(value) = op.get_increment_value() {
// only observe this increment if the counter is visible, i.e. the counter's
// create op is in the values
if values.iter().any(|value| op.pred.contains(&value.id)) {
// we have observed the value
observer.increment(ex_obj, key, (value, self.id_to_exid(op.id)));
observer.increment(parents, ex_obj, key, (value, self.id_to_exid(op.id)));
}
} else {
let winner = if let Some(last_value) = values.last() {
@ -280,10 +300,10 @@ impl OpSetInternal {
};
let value = (winner.value(), self.id_to_exid(winner.id));
if op.is_list_op() && !had_value_before {
observer.insert(ex_obj, seen, value);
observer.insert(parents, ex_obj, seen, value);
} else {
let conflict = !values.is_empty();
observer.put(ex_obj, key, value, conflict);
observer.put(parents, ex_obj, key, value, conflict);
}
}

View file

@ -1,16 +0,0 @@
#[derive(Debug, Default)]
pub struct ApplyOptions<'a, Obs> {
pub op_observer: Option<&'a mut Obs>,
}
impl<'a, Obs> ApplyOptions<'a, Obs> {
pub fn with_op_observer(mut self, op_observer: &'a mut Obs) -> Self {
self.op_observer = Some(op_observer);
self
}
pub fn set_op_observer(&mut self, op_observer: &'a mut Obs) -> &mut Self {
self.op_observer = Some(op_observer);
self
}
}

View file

@ -1,18 +1,33 @@
use crate::{exid::ExId, types::ObjId, Automerge, Prop};
use crate::op_set::OpSet;
use crate::types::ObjId;
use crate::{exid::ExId, Prop};
#[derive(Debug)]
pub struct Parents<'a> {
pub(crate) obj: ObjId,
pub(crate) doc: &'a Automerge,
pub(crate) ops: &'a OpSet,
}
impl<'a> Parents<'a> {
pub fn path(&mut self) -> Vec<(ExId, Prop)> {
let mut path = self.collect::<Vec<_>>();
path.reverse();
path
}
}
impl<'a> Iterator for Parents<'a> {
type Item = (ExId, Prop);
fn next(&mut self) -> Option<Self::Item> {
if let Some((obj, key)) = self.doc.parent_object(self.obj) {
if self.obj.is_root() {
None
} else if let Some((obj, key)) = self.ops.parent_object(&self.obj) {
self.obj = obj;
Some((self.doc.id_to_exid(obj.0), self.doc.export_key(obj, key)))
Some((
self.ops.id_to_exid(self.obj.0),
self.ops.export_key(self.obj, key),
))
} else {
None
}

View file

@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
use crate::{
storage::{parse, Change as StoredChange, ReadChangeOpError},
ApplyOptions, Automerge, AutomergeError, Change, ChangeHash, OpObserver,
Automerge, AutomergeError, Change, ChangeHash, OpObserver,
};
mod bloom;
@ -104,14 +104,14 @@ impl Automerge {
sync_state: &mut State,
message: Message,
) -> Result<(), AutomergeError> {
self.receive_sync_message_with::<()>(sync_state, message, ApplyOptions::default())
self.receive_sync_message_with::<()>(sync_state, message, None)
}
pub fn receive_sync_message_with<'a, Obs: OpObserver>(
pub fn receive_sync_message_with<Obs: OpObserver>(
&mut self,
sync_state: &mut State,
message: Message,
options: ApplyOptions<'a, Obs>,
op_observer: Option<&mut Obs>,
) -> Result<(), AutomergeError> {
let before_heads = self.get_heads();
@ -124,7 +124,7 @@ impl Automerge {
let changes_is_empty = message_changes.is_empty();
if !changes_is_empty {
self.apply_changes_with(message_changes, options)?;
self.apply_changes_with(message_changes, op_observer)?;
sync_state.shared_heads = advance_heads(
&before_heads.iter().collect(),
&self.get_heads().into_iter().collect(),

View file

@ -11,4 +11,4 @@ pub use manual_transaction::Transaction;
pub use result::Failure;
pub use result::Success;
pub type Result<O, E> = std::result::Result<Success<O>, Failure<E>>;
pub type Result<O, Obs, E> = std::result::Result<Success<O, Obs>, Failure<E>>;

View file

@ -1,12 +1,11 @@
/// Optional metadata for a commit.
#[derive(Debug, Default)]
pub struct CommitOptions<'a, Obs> {
pub struct CommitOptions {
pub message: Option<String>,
pub time: Option<i64>,
pub op_observer: Option<&'a mut Obs>,
}
impl<'a, Obs> CommitOptions<'a, Obs> {
impl CommitOptions {
/// Add a message to the commit.
pub fn with_message<S: Into<String>>(mut self, message: S) -> Self {
self.message = Some(message.into());
@ -30,14 +29,4 @@ impl<'a, Obs> CommitOptions<'a, Obs> {
self.time = Some(time);
self
}
pub fn with_op_observer(mut self, op_observer: &'a mut Obs) -> Self {
self.op_observer = Some(op_observer);
self
}
pub fn set_op_observer(&mut self, op_observer: &'a mut Obs) -> &mut Self {
self.op_observer = Some(op_observer);
self
}
}

View file

@ -26,13 +26,12 @@ impl TransactionInner {
/// Commit the operations performed in this transaction, returning the hashes corresponding to
/// the new heads.
#[tracing::instrument(skip(self, doc, op_observer))]
pub(crate) fn commit<Obs: OpObserver>(
#[tracing::instrument(skip(self, doc))]
pub(crate) fn commit(
mut self,
doc: &mut Automerge,
message: Option<String>,
time: Option<i64>,
op_observer: Option<&mut Obs>,
) -> ChangeHash {
if message.is_some() {
self.message = message;
@ -42,26 +41,6 @@ impl TransactionInner {
self.time = t;
}
if let Some(observer) = op_observer {
for (obj, prop, op) in &self.operations {
let ex_obj = doc.ops.id_to_exid(obj.0);
if op.insert {
let value = (op.value(), doc.id_to_exid(op.id));
match prop {
Prop::Map(_) => panic!("insert into a map"),
Prop::Seq(index) => observer.insert(ex_obj, *index, value),
}
} else if op.is_delete() {
observer.delete(ex_obj, prop.clone());
} else if let Some(value) = op.get_increment_value() {
observer.increment(ex_obj, prop.clone(), (value, doc.id_to_exid(op.id)));
} else {
let value = (op.value(), doc.ops.id_to_exid(op.id));
observer.put(ex_obj, prop.clone(), value, false);
}
}
}
let num_ops = self.pending_ops();
let change = self.export(&doc.ops.m);
let hash = change.hash();
@ -150,9 +129,10 @@ impl TransactionInner {
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>>(
pub(crate) fn put<P: Into<Prop>, V: Into<ScalarValue>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
value: V,
@ -160,7 +140,7 @@ impl TransactionInner {
let obj = doc.exid_to_obj(ex_obj)?;
let value = value.into();
let prop = prop.into();
self.local_op(doc, obj, prop, value.into())?;
self.local_op(doc, op_observer, obj, prop, value.into())?;
Ok(())
}
@ -177,16 +157,19 @@ impl TransactionInner {
/// - The object does not exist
/// - The key is the wrong type for the object
/// - The key does not exist in the object
pub(crate) fn put_object<P: Into<Prop>>(
pub(crate) fn put_object<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
value: ObjType,
) -> Result<ExId, AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let prop = prop.into();
let id = self.local_op(doc, obj, prop, value.into())?.unwrap();
let id = self
.local_op(doc, op_observer, obj, prop, value.into())?
.unwrap();
let id = doc.id_to_exid(id);
Ok(id)
}
@ -195,9 +178,11 @@ impl TransactionInner {
OpId(self.start_op.get() + self.pending_ops() as u64, self.actor)
}
fn insert_local_op(
#[allow(clippy::too_many_arguments)]
fn insert_local_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
prop: Prop,
op: Op,
pos: usize,
@ -210,12 +195,13 @@ impl TransactionInner {
doc.ops.insert(pos, &obj, op.clone());
}
self.operations.push((obj, prop, op));
self.finalize_op(doc, op_observer, obj, prop, op);
}
pub(crate) fn insert<V: Into<ScalarValue>>(
pub(crate) fn insert<V: Into<ScalarValue>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
index: usize,
value: V,
@ -223,26 +209,28 @@ impl TransactionInner {
let obj = doc.exid_to_obj(ex_obj)?;
let value = value.into();
tracing::trace!(obj=?obj, value=?value, "inserting value");
self.do_insert(doc, obj, index, value.into())?;
self.do_insert(doc, op_observer, obj, index, value.into())?;
Ok(())
}
pub(crate) fn insert_object(
pub(crate) fn insert_object<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
index: usize,
value: ObjType,
) -> Result<ExId, AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let id = self.do_insert(doc, obj, index, value.into())?;
let id = self.do_insert(doc, op_observer, obj, index, value.into())?;
let id = doc.id_to_exid(id);
Ok(id)
}
fn do_insert(
fn do_insert<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
index: usize,
action: OpType,
@ -263,27 +251,30 @@ impl TransactionInner {
};
doc.ops.insert(query.pos(), &obj, op.clone());
self.operations.push((obj, Prop::Seq(index), op));
self.finalize_op(doc, op_observer, obj, Prop::Seq(index), op);
Ok(id)
}
pub(crate) fn local_op(
pub(crate) fn local_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: Prop,
action: OpType,
) -> Result<Option<OpId>, AutomergeError> {
match prop {
Prop::Map(s) => self.local_map_op(doc, obj, s, action),
Prop::Seq(n) => self.local_list_op(doc, obj, n, action),
Prop::Map(s) => self.local_map_op(doc, op_observer, obj, s, action),
Prop::Seq(n) => self.local_list_op(doc, op_observer, obj, n, action),
}
}
fn local_map_op(
fn local_map_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: String,
action: OpType,
@ -324,14 +315,15 @@ impl TransactionInner {
let pos = query.pos;
let ops_pos = query.ops_pos;
self.insert_local_op(doc, Prop::Map(prop), op, pos, obj, &ops_pos);
self.insert_local_op(doc, op_observer, Prop::Map(prop), op, pos, obj, &ops_pos);
Ok(Some(id))
}
fn local_list_op(
fn local_list_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
index: usize,
action: OpType,
@ -363,40 +355,43 @@ impl TransactionInner {
let pos = query.pos;
let ops_pos = query.ops_pos;
self.insert_local_op(doc, Prop::Seq(index), op, pos, obj, &ops_pos);
self.insert_local_op(doc, op_observer, Prop::Seq(index), op, pos, obj, &ops_pos);
Ok(Some(id))
}
pub(crate) fn increment<P: Into<Prop>>(
pub(crate) fn increment<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: &ExId,
prop: P,
value: i64,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(obj)?;
self.local_op(doc, obj, prop.into(), OpType::Increment(value))?;
self.local_op(doc, op_observer, obj, prop.into(), OpType::Increment(value))?;
Ok(())
}
pub(crate) fn delete<P: Into<Prop>>(
pub(crate) fn delete<P: Into<Prop>, Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
prop: P,
) -> Result<(), AutomergeError> {
let obj = doc.exid_to_obj(ex_obj)?;
let prop = prop.into();
self.local_op(doc, obj, prop, OpType::Delete)?;
self.local_op(doc, op_observer, obj, prop, OpType::Delete)?;
Ok(())
}
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
/// the new elements
pub(crate) fn splice(
pub(crate) fn splice<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
ex_obj: &ExId,
mut pos: usize,
del: usize,
@ -405,15 +400,48 @@ impl TransactionInner {
let obj = doc.exid_to_obj(ex_obj)?;
for _ in 0..del {
// del()
self.local_op(doc, obj, pos.into(), OpType::Delete)?;
self.local_op(doc, op_observer, obj, pos.into(), OpType::Delete)?;
}
for v in vals {
// insert()
self.do_insert(doc, obj, pos, v.clone().into())?;
self.do_insert(doc, op_observer, obj, pos, v.clone().into())?;
pos += 1;
}
Ok(())
}
fn finalize_op<Obs: OpObserver>(
&mut self,
doc: &mut Automerge,
op_observer: &mut Obs,
obj: ObjId,
prop: Prop,
op: Op,
) {
// TODO - id_to_exid should be a noop if not used - change type to Into<ExId>?
let ex_obj = doc.ops.id_to_exid(obj.0);
let parents = doc.ops.parents(obj);
if op.insert {
let value = (op.value(), doc.ops.id_to_exid(op.id));
match prop {
Prop::Map(_) => panic!("insert into a map"),
Prop::Seq(index) => op_observer.insert(parents, ex_obj, index, value),
}
} else if op.is_delete() {
op_observer.delete(parents, ex_obj, prop.clone());
} else if let Some(value) = op.get_increment_value() {
op_observer.increment(
parents,
ex_obj,
prop.clone(),
(value, doc.ops.id_to_exid(op.id)),
);
} else {
let value = (op.value(), doc.ops.id_to_exid(op.id));
op_observer.put(parents, ex_obj, prop.clone(), value, false);
}
self.operations.push((obj, prop, op));
}
}
#[cfg(test)]

View file

@ -20,14 +20,15 @@ use super::{CommitOptions, Transactable, TransactionInner};
/// intermediate state.
/// This is consistent with `?` error handling.
#[derive(Debug)]
pub struct Transaction<'a> {
pub struct Transaction<'a, Obs: OpObserver> {
// this is an option so that we can take it during commit and rollback to prevent it being
// rolled back during drop.
pub(crate) inner: Option<TransactionInner>,
pub(crate) doc: &'a mut Automerge,
pub op_observer: Obs,
}
impl<'a> Transaction<'a> {
impl<'a, Obs: OpObserver> Transaction<'a, Obs> {
/// Get the heads of the document before this transaction was started.
pub fn get_heads(&self) -> Vec<ChangeHash> {
self.doc.get_heads()
@ -36,10 +37,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) -> ChangeHash {
self.inner
.take()
.unwrap()
.commit::<()>(self.doc, None, None, None)
self.inner.take().unwrap().commit(self.doc, None, None)
}
/// Commit the operations in this transaction with some options.
@ -56,15 +54,13 @@ impl<'a> Transaction<'a> {
/// tx.put_object(ROOT, "todos", ObjType::List).unwrap();
/// let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs() as
/// i64;
/// tx.commit_with::<()>(CommitOptions::default().with_message("Create todos list").with_time(now));
/// tx.commit_with(CommitOptions::default().with_message("Create todos list").with_time(now));
/// ```
pub fn commit_with<Obs: OpObserver>(mut self, options: CommitOptions<'_, Obs>) -> ChangeHash {
self.inner.take().unwrap().commit(
self.doc,
options.message,
options.time,
options.op_observer,
)
pub fn commit_with(mut self, options: CommitOptions) -> ChangeHash {
self.inner
.take()
.unwrap()
.commit(self.doc, options.message, options.time)
}
/// Undo the operations added in this transaction, returning the number of cancelled
@ -74,7 +70,7 @@ impl<'a> Transaction<'a> {
}
}
impl<'a> Transactable for Transaction<'a> {
impl<'a, Obs: OpObserver> Transactable for Transaction<'a, Obs> {
/// Get the number of pending operations in this transaction.
fn pending_ops(&self) -> usize {
self.inner.as_ref().unwrap().pending_ops()
@ -97,7 +93,7 @@ impl<'a> Transactable for Transaction<'a> {
self.inner
.as_mut()
.unwrap()
.put(self.doc, obj.as_ref(), prop, value)
.put(self.doc, &mut self.op_observer, obj.as_ref(), prop, value)
}
fn put_object<O: AsRef<ExId>, P: Into<Prop>>(
@ -106,10 +102,13 @@ impl<'a> Transactable for Transaction<'a> {
prop: P,
value: ObjType,
) -> Result<ExId, AutomergeError> {
self.inner
.as_mut()
.unwrap()
.put_object(self.doc, obj.as_ref(), prop, value)
self.inner.as_mut().unwrap().put_object(
self.doc,
&mut self.op_observer,
obj.as_ref(),
prop,
value,
)
}
fn insert<O: AsRef<ExId>, V: Into<ScalarValue>>(
@ -118,10 +117,13 @@ impl<'a> Transactable for Transaction<'a> {
index: usize,
value: V,
) -> Result<(), AutomergeError> {
self.inner
.as_mut()
.unwrap()
.insert(self.doc, obj.as_ref(), index, value)
self.inner.as_mut().unwrap().insert(
self.doc,
&mut self.op_observer,
obj.as_ref(),
index,
value,
)
}
fn insert_object<O: AsRef<ExId>>(
@ -130,10 +132,13 @@ impl<'a> Transactable for Transaction<'a> {
index: usize,
value: ObjType,
) -> Result<ExId, AutomergeError> {
self.inner
.as_mut()
.unwrap()
.insert_object(self.doc, obj.as_ref(), index, value)
self.inner.as_mut().unwrap().insert_object(
self.doc,
&mut self.op_observer,
obj.as_ref(),
index,
value,
)
}
fn increment<O: AsRef<ExId>, P: Into<Prop>>(
@ -142,10 +147,13 @@ impl<'a> Transactable for Transaction<'a> {
prop: P,
value: i64,
) -> Result<(), AutomergeError> {
self.inner
.as_mut()
.unwrap()
.increment(self.doc, obj.as_ref(), prop, value)
self.inner.as_mut().unwrap().increment(
self.doc,
&mut self.op_observer,
obj.as_ref(),
prop,
value,
)
}
fn delete<O: AsRef<ExId>, P: Into<Prop>>(
@ -156,7 +164,7 @@ impl<'a> Transactable for Transaction<'a> {
self.inner
.as_mut()
.unwrap()
.delete(self.doc, obj.as_ref(), prop)
.delete(self.doc, &mut self.op_observer, obj.as_ref(), prop)
}
/// Splice new elements into the given sequence. Returns a vector of the OpIds used to insert
@ -168,10 +176,14 @@ impl<'a> Transactable for Transaction<'a> {
del: usize,
vals: V,
) -> Result<(), AutomergeError> {
self.inner
.as_mut()
.unwrap()
.splice(self.doc, obj.as_ref(), pos, del, vals)
self.inner.as_mut().unwrap().splice(
self.doc,
&mut self.op_observer,
obj.as_ref(),
pos,
del,
vals,
)
}
fn keys<O: AsRef<ExId>>(&self, obj: O) -> Keys<'_, '_> {
@ -291,7 +303,7 @@ impl<'a> Transactable for Transaction<'a> {
// intermediate state.
// This defaults to rolling back the transaction to be compatible with `?` error returning before
// reaching a call to `commit`.
impl<'a> Drop for Transaction<'a> {
impl<'a, Obs: OpObserver> Drop for Transaction<'a, Obs> {
fn drop(&mut self) {
if let Some(txn) = self.inner.take() {
txn.rollback(self.doc);

View file

@ -2,11 +2,12 @@ use crate::ChangeHash;
/// The result of a successful, and committed, transaction.
#[derive(Debug)]
pub struct Success<O> {
pub struct Success<O, Obs> {
/// The result of the transaction.
pub result: O,
/// The hash of the change, also the head of the document.
pub hash: ChangeHash,
pub op_observer: Obs,
}
/// The result of a failed, and rolled back, transaction.

View file

@ -1,7 +1,7 @@
use automerge::transaction::Transactable;
use automerge::{
ActorId, ApplyOptions, AutoCommit, Automerge, AutomergeError, Change, ExpandedChange, ObjType,
ScalarValue, VecOpObserver, ROOT,
ActorId, AutoCommit, Automerge, AutomergeError, Change, ExpandedChange, ObjType, ScalarValue,
VecOpObserver, ROOT,
};
// set up logging for all the tests
@ -1005,13 +1005,8 @@ fn observe_counter_change_application() {
doc.increment(ROOT, "counter", 5).unwrap();
let changes = doc.get_changes(&[]).unwrap().into_iter().cloned();
let mut doc = AutoCommit::new();
let mut observer = VecOpObserver::default();
doc.apply_changes_with(
changes,
ApplyOptions::default().with_op_observer(&mut observer),
)
.unwrap();
let mut doc = AutoCommit::new().with_observer(VecOpObserver::default());
doc.apply_changes(changes).unwrap();
}
#[test]