From 9c63f073c30de96361a7efea7ade70f14e333ae3 Mon Sep 17 00:00:00 2001 From: Zixuan Chen Date: Mon, 19 Jan 2026 09:12:33 +0800 Subject: [PATCH] fix(undo): don't transform counter diffs Counter diffs commute, so transforming them can incorrectly cancel local undo after remote increments. Add regression test for issue #905. --- crates/loro-internal/src/event.rs | 8 ++------ crates/loro/tests/issue.rs | 34 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/crates/loro-internal/src/event.rs b/crates/loro-internal/src/event.rs index 063f3d354..9430c3f52 100644 --- a/crates/loro-internal/src/event.rs +++ b/crates/loro-internal/src/event.rs @@ -431,12 +431,8 @@ impl Diff { (Diff::Map(a), Diff::Map(b)) => a.transform(b, left_prior), (Diff::Tree(a), Diff::Tree(b)) => a.transform(b, left_prior), #[cfg(feature = "counter")] - (Diff::Counter(a), Diff::Counter(b)) => { - if left_prior { - *a += b; - } else { - *a -= b; - } + (Diff::Counter(_a), Diff::Counter(_b)) => { + // Counter operations commute; no transformation is needed. } _ => {} } diff --git a/crates/loro/tests/issue.rs b/crates/loro/tests/issue.rs index b0301ca8f..80ce09596 100644 --- a/crates/loro/tests/issue.rs +++ b/crates/loro/tests/issue.rs @@ -220,6 +220,40 @@ fn test_event_hint_merge_bug_clear_demonstration() { assert_eq!(text_b.to_string(), "bcde"); } +#[test] +fn test_undo_counter_after_remote_update_issue_905() { + let doc_a = LoroDoc::new(); + doc_a.set_peer_id(1).unwrap(); + let mut undo_manager = UndoManager::new(&doc_a); + undo_manager.set_merge_interval(0); + + let counter_a = doc_a.get_counter("counter"); + counter_a.increment(1.0).unwrap(); + doc_a.commit(); + + let doc_b = LoroDoc::new(); + doc_b.set_peer_id(2).unwrap(); + doc_b.import(&doc_a.export(ExportMode::all_updates()).unwrap()) + .unwrap(); + + let counter_b = doc_b.get_counter("counter"); + assert_eq!(counter_b.get_value(), 1.0); + counter_b.increment(1.0).unwrap(); + doc_b.commit(); + + doc_a.import(&doc_b.export(ExportMode::all_updates()).unwrap()) + .unwrap(); + assert_eq!(counter_a.get_value(), 2.0); + + assert!(undo_manager.can_undo()); + assert!(undo_manager.undo().unwrap()); + assert_eq!(counter_a.get_value(), 1.0); + + assert!(undo_manager.can_redo()); + assert!(undo_manager.redo().unwrap()); + assert_eq!(counter_a.get_value(), 2.0); +} + #[test] fn import_twice() { let doc = LoroDoc::new();