Skip to content

Commit e36f3c2

Browse files
authored
Merge pull request automerge#347 from jeffa5/observer-counters
Add increment observation for observer
2 parents 1fc5e55 + 1bee30c commit e36f3c2

File tree

10 files changed

+213
-9
lines changed

10 files changed

+213
-9
lines changed

automerge-wasm/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,13 @@ impl Automerge {
464464
};
465465
}
466466

467+
Patch::Increment { obj, key, value } => {
468+
js_set(&patch, "action", "increment")?;
469+
js_set(&patch, "obj", obj.to_string())?;
470+
js_set(&patch, "key", key)?;
471+
js_set(&patch, "value", value.0)?;
472+
}
473+
467474
Patch::Delete { obj, key } => {
468475
js_set(&patch, "action", "delete")?;
469476
js_set(&patch, "obj", obj.to_string())?;

automerge-wasm/test/test.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -956,15 +956,15 @@ describe('Automerge', () => {
956956
doc1.free()
957957
})
958958

959-
it.skip('should capture local increment ops', () => {
959+
it('should capture local increment ops', () => {
960960
let doc1 = create('aaaa')
961961
doc1.enablePatches(true)
962962
doc1.put('_root', 'counter', 2, 'counter')
963963
doc1.increment('_root', 'counter', 4)
964964

965965
assert.deepEqual(doc1.popPatches(), [
966966
{action: 'put', obj: '_root', key: 'counter', value: 2, datatype: 'counter', conflict: false},
967-
{action: 'put', obj: '_root', key: 'counter', value: 6, datatype: 'counter', conflict: false},
967+
{action: 'increment', obj: '_root', key: 'counter', value: 4},
968968
])
969969
doc1.free()
970970
})
@@ -986,23 +986,41 @@ describe('Automerge', () => {
986986
doc1.free()
987987
})
988988

989-
it.skip('should support counters in a map', () => {
989+
it('should support counters in a map', () => {
990990
let doc1 = create('aaaa'), doc2 = create('bbbb')
991991
doc2.enablePatches(true)
992992
doc1.put('_root', 'starlings', 2, 'counter')
993993
doc2.loadIncremental(doc1.saveIncremental())
994994
doc1.increment('_root', 'starlings', 1)
995-
doc1.dump()
996995
doc2.loadIncremental(doc1.saveIncremental())
997996
assert.deepEqual(doc2.get('_root', 'starlings'), ['counter', 3])
998997
assert.deepEqual(doc2.popPatches(), [
999998
{action: 'put', obj: '_root', key: 'starlings', value: 2, datatype: 'counter', conflict: false},
1000-
{action: 'put', obj: '_root', key: 'starlings', value: 3, datatype: 'counter', conflict: false}
999+
{action: 'increment', obj: '_root', key: 'starlings', value: 1}
10011000
])
10021001
doc1.free(); doc2.free()
10031002
})
10041003

1005-
it('should support counters in a list') // TODO
1004+
it('should support counters in a list', () => {
1005+
let doc1 = create('aaaa'), doc2 = create('bbbb')
1006+
doc2.enablePatches(true)
1007+
const list = doc1.putObject('_root', 'list', [])
1008+
doc2.loadIncremental(doc1.saveIncremental())
1009+
doc1.insert(list, 0, 1, 'counter')
1010+
doc2.loadIncremental(doc1.saveIncremental())
1011+
doc1.increment(list, 0, 2)
1012+
doc2.loadIncremental(doc1.saveIncremental())
1013+
doc1.increment(list, 0, -5)
1014+
doc2.loadIncremental(doc1.saveIncremental())
1015+
1016+
assert.deepEqual(doc2.popPatches(), [
1017+
{action: 'put', obj: '_root', key: 'list', value: list, datatype: 'list', conflict: false},
1018+
{action: 'insert', obj: list, key: 0, value: 1, datatype: 'counter'},
1019+
{action: 'increment', obj: list, key: 0, value: 2},
1020+
{action: 'increment', obj: list, key: 0, value: -5},
1021+
])
1022+
doc1.free(); doc2.free()
1023+
})
10061024

10071025
it('should delete a counter from a map') // TODO
10081026
})

automerge/examples/watch.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ fn get_changes(doc: &Automerge, patches: Vec<Patch>) {
6666
doc.path_to_object(&obj)
6767
)
6868
}
69+
Patch::Increment { obj, key, value } => {
70+
println!(
71+
"increment {:?} in obj {:?} by {:?}, object path {:?}",
72+
key,
73+
obj,
74+
value,
75+
doc.path_to_object(&obj)
76+
)
77+
}
6978
Patch::Delete { obj, key } => println!(
7079
"delete {:?} in obj {:?}, object path {:?}",
7180
key,

automerge/src/automerge.rs

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ impl Automerge {
10041004

10051005
pub fn dump(&self) {
10061006
log!(
1007-
" {:12} {:12} {:12} {} {} {}",
1007+
" {:12} {:12} {:12} {:12} {:12} {:12}",
10081008
"id",
10091009
"obj",
10101010
"key",
@@ -1028,7 +1028,7 @@ impl Automerge {
10281028
let pred: Vec<_> = op.pred.iter().map(|id| self.to_string(*id)).collect();
10291029
let succ: Vec<_> = op.succ.iter().map(|id| self.to_string(*id)).collect();
10301030
log!(
1031-
" {:12} {:12} {:12} {} {:?} {:?}",
1031+
" {:12} {:12} {:12} {:12} {:12?} {:12?}",
10321032
id,
10331033
obj,
10341034
key,
@@ -2005,4 +2005,96 @@ mod tests {
20052005
let len = doc.length(&text);
20062006
assert_eq!(len, 4); // 4 chars
20072007
}
2008+
2009+
#[test]
2010+
fn observe_counter_change_application_overwrite() {
2011+
let mut doc1 = AutoCommit::new();
2012+
doc1.set_actor(ActorId::from([1]));
2013+
doc1.put(ROOT, "counter", ScalarValue::counter(1)).unwrap();
2014+
doc1.commit();
2015+
2016+
let mut doc2 = doc1.fork();
2017+
doc2.set_actor(ActorId::from([2]));
2018+
doc2.put(ROOT, "counter", "mystring").unwrap();
2019+
doc2.commit();
2020+
2021+
doc1.increment(ROOT, "counter", 2).unwrap();
2022+
doc1.commit();
2023+
doc1.increment(ROOT, "counter", 5).unwrap();
2024+
doc1.commit();
2025+
2026+
let mut observer = VecOpObserver::default();
2027+
let mut doc3 = doc1.clone();
2028+
doc3.merge_with(
2029+
&mut doc2,
2030+
ApplyOptions::default().with_op_observer(&mut observer),
2031+
)
2032+
.unwrap();
2033+
2034+
assert_eq!(
2035+
observer.take_patches(),
2036+
vec![Patch::Put {
2037+
obj: ExId::Root,
2038+
key: Prop::Map("counter".into()),
2039+
value: (
2040+
ScalarValue::Str("mystring".into()).into(),
2041+
ExId::Id(2, doc2.get_actor().clone(), 1)
2042+
),
2043+
conflict: false
2044+
}]
2045+
);
2046+
2047+
let mut observer = VecOpObserver::default();
2048+
let mut doc4 = doc2.clone();
2049+
doc4.merge_with(
2050+
&mut doc1,
2051+
ApplyOptions::default().with_op_observer(&mut observer),
2052+
)
2053+
.unwrap();
2054+
2055+
// no patches as the increments operate on an invisible counter
2056+
assert_eq!(observer.take_patches(), vec![]);
2057+
}
2058+
2059+
#[test]
2060+
fn observe_counter_change_application() {
2061+
let mut doc = AutoCommit::new();
2062+
doc.put(ROOT, "counter", ScalarValue::counter(1)).unwrap();
2063+
doc.increment(ROOT, "counter", 2).unwrap();
2064+
doc.increment(ROOT, "counter", 5).unwrap();
2065+
let changes = doc.get_changes(&[]).into_iter().cloned().collect();
2066+
2067+
let mut new_doc = AutoCommit::new();
2068+
let mut observer = VecOpObserver::default();
2069+
new_doc
2070+
.apply_changes_with(
2071+
changes,
2072+
ApplyOptions::default().with_op_observer(&mut observer),
2073+
)
2074+
.unwrap();
2075+
assert_eq!(
2076+
observer.take_patches(),
2077+
vec![
2078+
Patch::Put {
2079+
obj: ExId::Root,
2080+
key: Prop::Map("counter".into()),
2081+
value: (
2082+
ScalarValue::counter(1).into(),
2083+
ExId::Id(1, doc.get_actor().clone(), 0)
2084+
),
2085+
conflict: false
2086+
},
2087+
Patch::Increment {
2088+
obj: ExId::Root,
2089+
key: Prop::Map("counter".into()),
2090+
value: (2, ExId::Id(2, doc.get_actor().clone(), 0)),
2091+
},
2092+
Patch::Increment {
2093+
obj: ExId::Root,
2094+
key: Prop::Map("counter".into()),
2095+
value: (5, ExId::Id(3, doc.get_actor().clone(), 0)),
2096+
}
2097+
]
2098+
);
2099+
}
20082100
}

automerge/src/op_observer.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ pub trait OpObserver {
2121
/// - `conflict`: whether this put conflicts with other operations.
2222
fn put(&mut self, objid: ExId, key: Prop, tagged_value: (Value, ExId), conflict: bool);
2323

24+
/// A counter has been incremented.
25+
///
26+
/// - `objid`: the object that contains the counter.
27+
/// - `key`: they key that the chounter is at.
28+
/// - `tagged_value`: the amount the counter has been incremented by, and the the id of the
29+
/// increment operation.
30+
fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId));
31+
2432
/// A value has beeen deleted.
2533
///
2634
/// - `objid`: the object that has been deleted in.
@@ -33,6 +41,8 @@ impl OpObserver for () {
3341

3442
fn put(&mut self, _objid: ExId, _key: Prop, _tagged_value: (Value, ExId), _conflict: bool) {}
3543

44+
fn increment(&mut self, _objid: ExId, _key: Prop, _tagged_value: (i64, ExId)) {}
45+
3646
fn delete(&mut self, _objid: ExId, _key: Prop) {}
3747
}
3848

@@ -68,6 +78,14 @@ impl OpObserver for VecOpObserver {
6878
});
6979
}
7080

81+
fn increment(&mut self, objid: ExId, key: Prop, tagged_value: (i64, ExId)) {
82+
self.patches.push(Patch::Increment {
83+
obj: objid,
84+
key,
85+
value: tagged_value,
86+
});
87+
}
88+
7189
fn delete(&mut self, objid: ExId, key: Prop) {
7290
self.patches.push(Patch::Delete { obj: objid, key })
7391
}
@@ -96,6 +114,16 @@ pub enum Patch {
96114
/// The value that was inserted, and the id of the operation that inserted it there.
97115
value: (Value<'static>, ExId),
98116
},
117+
/// Incrementing a counter.
118+
Increment {
119+
/// The object that was incremented in.
120+
obj: ExId,
121+
/// The key that was incremented.
122+
key: Prop,
123+
/// The amount that the counter was incremented by, and the id of the operation that
124+
/// did the increment.
125+
value: (i64, ExId),
126+
},
99127
/// Deleting an element from a list/text
100128
Delete {
101129
/// The object that was deleted from.

automerge/src/op_set.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ impl OpSetInternal {
202202
} else {
203203
observer.delete(ex_obj, key);
204204
}
205+
} else if let Some(value) = op.get_increment_value() {
206+
// only observe this increment if the counter is visible, i.e. the counter's
207+
// create op is in the values
208+
if values.iter().any(|value| op.pred.contains(&value.id)) {
209+
// we have observed the value
210+
observer.increment(ex_obj, key, (value, self.id_to_exid(op.id)));
211+
}
205212
} else {
206213
let winner = if let Some(last_value) = values.last() {
207214
if self.m.lamport_cmp(op.id, last_value.id) == Ordering::Greater {

automerge/src/query/seek_op_with_patch.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
135135

136136
// Keep track of any ops we're overwriting and any conflicts on this key
137137
if self.op.overwrites(op) {
138+
// when we encounter an increment op we also want to find the counter for
139+
// it.
140+
if self.op.is_inc() && op.is_counter() && op.visible() {
141+
self.values.push(op);
142+
}
138143
self.succ.push(self.pos);
139144
} else if op.visible() {
140145
self.values.push(op);
@@ -145,6 +150,7 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
145150
if m.lamport_cmp(op.id, self.op.id) == Ordering::Greater {
146151
break;
147152
}
153+
148154
self.pos += 1;
149155
}
150156

@@ -178,6 +184,11 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
178184
if self.is_target_insert(e) {
179185
self.found = true;
180186
if self.op.overwrites(e) {
187+
// when we encounter an increment op we also want to find the counter for
188+
// it.
189+
if self.op.is_inc() && e.is_counter() && e.visible() {
190+
self.values.push(e);
191+
}
181192
self.succ.push(self.pos);
182193
}
183194
if e.visible() {
@@ -190,6 +201,11 @@ impl<'a> TreeQuery<'a> for SeekOpWithPatch<'a> {
190201
// Once we've found the reference element, keep track of any ops that we're overwriting
191202
let overwritten = self.op.overwrites(e);
192203
if overwritten {
204+
// when we encounter an increment op we also want to find the counter for
205+
// it.
206+
if self.op.is_inc() && e.is_counter() && e.visible() {
207+
self.values.push(e);
208+
}
193209
self.succ.push(self.pos);
194210
}
195211

automerge/src/transaction/inner.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ impl TransactionInner {
5353
}
5454
} else if op.is_delete() {
5555
observer.delete(ex_obj, prop.clone());
56+
} else if let Some(value) = op.get_increment_value() {
57+
observer.increment(ex_obj, prop.clone(), (value, doc.id_to_exid(op.id)));
5658
} else {
5759
let value = (op.value(), doc.ops.id_to_exid(op.id));
5860
observer.put(ex_obj, prop.clone(), value, false);

automerge/src/types.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,14 @@ impl Op {
459459
}
460460
}
461461

462+
pub fn get_increment_value(&self) -> Option<i64> {
463+
if let OpType::Increment(i) = self.action {
464+
Some(i)
465+
} else {
466+
None
467+
}
468+
}
469+
462470
pub fn value(&self) -> Value {
463471
match &self.action {
464472
OpType::Make(obj_type) => Value::Object(*obj_type),

automerge/tests/test.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use automerge::transaction::Transactable;
22
use automerge::{
3-
ActorId, AutoCommit, Automerge, AutomergeError, ObjType, ScalarValue, Value, ROOT,
3+
ActorId, ApplyOptions, AutoCommit, Automerge, AutomergeError, ObjType, ScalarValue, Value, VecOpObserver, ROOT,
44
};
55

66
mod helpers;
@@ -930,6 +930,23 @@ fn list_counter_del() -> Result<(), automerge::AutomergeError> {
930930
Ok(())
931931
}
932932

933+
#[test]
934+
fn observe_counter_change_application() {
935+
let mut doc = AutoCommit::new();
936+
doc.put(ROOT, "counter", ScalarValue::counter(1)).unwrap();
937+
doc.increment(ROOT, "counter", 2).unwrap();
938+
doc.increment(ROOT, "counter", 5).unwrap();
939+
let changes = doc.get_changes(&[]).into_iter().cloned().collect();
940+
941+
let mut doc = AutoCommit::new();
942+
let mut observer = VecOpObserver::default();
943+
doc.apply_changes_with(
944+
changes,
945+
ApplyOptions::default().with_op_observer(&mut observer),
946+
)
947+
.unwrap();
948+
}
949+
933950
#[test]
934951
fn increment_non_counter_map() {
935952
let mut doc = AutoCommit::new();

0 commit comments

Comments
 (0)