Skip to content

Commit 5bfffd7

Browse files
authored
fix(undo): trim empty front stack rows (#983)
1 parent 095ad56 commit 5bfffd7

4 files changed

Lines changed: 87 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"loro-crdt": patch
3+
---
4+
5+
Fix panic in `UndoManager` when `maxUndoSteps` trimming encounters an empty front stack row left by a prior undo with remote diffs.

crates/loro-internal/src/undo.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,18 +442,40 @@ impl Stack {
442442
self.size
443443
}
444444

445+
fn discard_empty_front_rows(&mut self) {
446+
// Undo pop can leave an empty front row that only carries remote diffs.
447+
// There is no older stack item for that diff to transform during trimming.
448+
while self
449+
.stack
450+
.front()
451+
.is_some_and(|(items, _)| items.is_empty())
452+
{
453+
self.stack.pop_front();
454+
}
455+
}
456+
457+
fn ensure_trailing_empty_row(&mut self) {
458+
if self.stack.is_empty() {
459+
self.stack
460+
.push_back((VecDeque::new(), Arc::new(Mutex::new(Default::default()))));
461+
}
462+
}
463+
445464
fn pop_front(&mut self) {
446465
if self.is_empty() {
447466
return;
448467
}
449468

469+
self.discard_empty_front_rows();
450470
self.size -= 1;
451471
let first = self.stack.front_mut().unwrap();
452472
let f = first.0.pop_front();
453473
assert!(f.is_some());
454474
if first.0.is_empty() {
455475
self.stack.pop_front();
456476
}
477+
478+
self.ensure_trailing_empty_row();
457479
}
458480

459481
fn set_top_meta(&mut self, meta: UndoItemMeta) {

crates/loro-wasm/tests/undo.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,35 @@ describe("undo", () => {
8080
expect(doc.getText("text").length).toBe(100);
8181
});
8282

83+
test("max undo steps after remote update and undo", () => {
84+
const doc = new LoroDoc();
85+
doc.setPeerId(1);
86+
const text = doc.getText("text");
87+
const undo = new UndoManager(doc, { maxUndoSteps: 3, mergeInterval: 0 });
88+
89+
text.insert(0, "A");
90+
doc.commit();
91+
92+
const remote = new LoroDoc();
93+
remote.setPeerId(2);
94+
remote.import(doc.export({ mode: "snapshot" }));
95+
remote.getText("text").insert(0, "R");
96+
remote.commit();
97+
98+
doc.import(remote.export({ mode: "update" }));
99+
expect(undo.undo()).toBeTruthy();
100+
expect(text.toString()).toBe("R");
101+
102+
for (let i = 0; i < 4; i++) {
103+
text.insert(text.length, i.toString());
104+
doc.commit();
105+
}
106+
107+
expect(doc.toJSON()).toStrictEqual({
108+
text: "R0123",
109+
});
110+
});
111+
83112
test("Skip chosen events", () => {
84113
const doc = new LoroDoc();
85114
const undo = new UndoManager(doc, {

crates/loro/tests/issue.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,37 @@ fn test_undo_counter_after_remote_update_issue_905() {
256256
assert_eq!(counter_a.get_value(), 2.0);
257257
}
258258

259+
#[test]
260+
fn undo_max_steps_trim_after_remote_undo_should_not_panic() -> Result<(), LoroError> {
261+
let doc_a = LoroDoc::new();
262+
doc_a.set_peer_id(1)?;
263+
let text_a = doc_a.get_text("text");
264+
let mut undo_manager = UndoManager::new(&doc_a);
265+
undo_manager.set_merge_interval(0);
266+
undo_manager.set_max_undo_steps(3);
267+
268+
text_a.insert(0, "A")?;
269+
doc_a.commit();
270+
271+
let doc_b = LoroDoc::from_snapshot(&doc_a.export(ExportMode::Snapshot)?)?;
272+
doc_b.set_peer_id(2)?;
273+
let text_b = doc_b.get_text("text");
274+
text_b.insert(0, "R")?;
275+
doc_b.commit();
276+
277+
doc_a.import(&doc_b.export(ExportMode::all_updates())?)?;
278+
assert!(undo_manager.undo()?);
279+
assert_eq!(text_a.to_string(), "R");
280+
281+
for i in 0..4 {
282+
text_a.insert(text_a.len_unicode(), &i.to_string())?;
283+
doc_a.commit();
284+
}
285+
286+
assert_eq!(undo_manager.undo_count(), 3);
287+
Ok(())
288+
}
289+
259290
#[test]
260291
fn import_twice() {
261292
let doc = LoroDoc::new();

0 commit comments

Comments
 (0)