Skip to content

Commit 9f68a57

Browse files
authored
fix: return errors for pre-shallow frontiers in diff and checkout (#931)
* fix: handle pre-shallow frontier errors in diff and checkout * chore: add changeset for shallow frontier error handling
1 parent 781bd26 commit 9f68a57

3 files changed

Lines changed: 99 additions & 15 deletions

File tree

.changeset/popular-ads-vanish.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"loro-crdt": patch
3+
"loro-crdt-map": patch
4+
---
5+
6+
Return errors instead of panicking when diff or checkout targets frontiers before a shallow snapshot root.

crates/loro-internal/src/loro.rs

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,18 +1006,25 @@ impl LoroDoc {
10061006
// FIXME: This method needs testing (no event should be emitted during processing this)
10071007
pub fn diff(&self, a: &Frontiers, b: &Frontiers) -> LoroResult<DiffBatch> {
10081008
{
1009-
// check whether a and b are valid
1009+
// Check whether a and b are valid before checkout so this returns a normal error
1010+
// instead of panicking on shallow docs.
10101011
let oplog = self.oplog.lock().unwrap();
1011-
for id in a.iter() {
1012-
if !oplog.dag.contains(id) {
1013-
return Err(LoroError::FrontiersNotFound(id));
1012+
let validate_frontiers = |frontiers: &Frontiers| -> LoroResult<()> {
1013+
for id in frontiers.iter() {
1014+
if !oplog.dag.contains(id) {
1015+
return Err(LoroError::FrontiersNotFound(id));
1016+
}
10141017
}
1015-
}
1016-
for id in b.iter() {
1017-
if !oplog.dag.contains(id) {
1018-
return Err(LoroError::FrontiersNotFound(id));
1018+
1019+
if oplog.dag.is_before_shallow_root(frontiers) {
1020+
return Err(LoroError::SwitchToVersionBeforeShallowRoot);
10191021
}
1020-
}
1022+
1023+
Ok(())
1024+
};
1025+
1026+
validate_frontiers(a)?;
1027+
validate_frontiers(b)?;
10211028
}
10221029

10231030
let (options, txn) = self.implicit_commit_then_stop();
@@ -1354,18 +1361,27 @@ impl LoroDoc {
13541361
/// This will make the current [DocState] detached from the latest version of [OpLog].
13551362
/// Any further import will not be reflected on the [DocState], until user call [LoroDoc::attach()]
13561363
pub fn checkout(&self, frontiers: &Frontiers) -> LoroResult<()> {
1364+
let was_detached = self.is_detached();
13571365
let (options, guard) = self.implicit_commit_then_stop();
1358-
self._checkout_without_emitting(frontiers, true, true)?;
1359-
self.emit_events();
1366+
let result = self._checkout_without_emitting(frontiers, true, true);
1367+
if result.is_ok() {
1368+
self.emit_events();
1369+
}
13601370
drop(guard);
13611371
if self.config.detached_editing() {
1362-
self.renew_peer_id();
1372+
if result.is_ok() {
1373+
self.renew_peer_id();
1374+
}
13631375
self.renew_txn_if_auto_commit(options);
1376+
} else if result.is_err() {
1377+
if !was_detached {
1378+
self.renew_txn_if_auto_commit(options);
1379+
}
13641380
} else if !self.is_detached() {
13651381
self.renew_txn_if_auto_commit(options);
13661382
}
13671383

1368-
Ok(())
1384+
result
13691385
}
13701386

13711387
/// NOTE: The caller of this method should ensure the txn is locked and set to None

crates/loro/tests/issue.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#![allow(deprecated)]
22
#![allow(unexpected_cfgs)]
33
use loro::{
4-
cursor::Cursor, ContainerID, ContainerTrait, EncodedBlobMode, ExportMode, LoroDoc, LoroList,
5-
LoroText, UndoManager,
4+
cursor::Cursor, ContainerID, ContainerTrait, EncodedBlobMode, ExportMode, LoroDoc, LoroError,
5+
LoroList, LoroText, UndoManager,
66
};
77
use std::sync::{Arc, Mutex};
88
use tracing::{trace, trace_span};
@@ -344,6 +344,68 @@ fn issue_822_tree_shallow_snapshot_roundtrip() {
344344
assert_eq!(doc_before, doc_after, "doc shallow value should roundtrip");
345345
}
346346

347+
#[test]
348+
fn issue_928_diff_before_shallow_root_should_error_without_poisoning_doc() {
349+
let doc = LoroDoc::new();
350+
doc.set_peer_id(1).unwrap();
351+
352+
let text = doc.get_text("t");
353+
text.insert(0, "hello").unwrap();
354+
doc.commit();
355+
let pre_shallow_frontiers = doc.oplog_frontiers();
356+
357+
text.insert(5, " world").unwrap();
358+
doc.commit();
359+
let current_frontiers = doc.oplog_frontiers();
360+
361+
let shallow_snapshot = doc
362+
.export(ExportMode::shallow_snapshot(&current_frontiers))
363+
.unwrap();
364+
let shallow_doc = LoroDoc::new();
365+
shallow_doc.import(&shallow_snapshot).unwrap();
366+
367+
let err = shallow_doc
368+
.diff(&pre_shallow_frontiers, &current_frontiers)
369+
.unwrap_err();
370+
assert_eq!(err, LoroError::SwitchToVersionBeforeShallowRoot);
371+
assert!(!shallow_doc.is_detached());
372+
assert_eq!(shallow_doc.get_text("t").to_string(), "hello world");
373+
374+
shallow_doc.get_text("t").insert(11, "!").unwrap();
375+
shallow_doc.commit();
376+
assert_eq!(shallow_doc.get_text("t").to_string(), "hello world!");
377+
}
378+
379+
#[test]
380+
fn issue_928_checkout_before_shallow_root_should_error_without_stopping_auto_commit() {
381+
let doc = LoroDoc::new();
382+
doc.set_peer_id(1).unwrap();
383+
384+
let text = doc.get_text("t");
385+
text.insert(0, "hello").unwrap();
386+
doc.commit();
387+
let pre_shallow_frontiers = doc.oplog_frontiers();
388+
389+
text.insert(5, " world").unwrap();
390+
doc.commit();
391+
let current_frontiers = doc.oplog_frontiers();
392+
393+
let shallow_snapshot = doc
394+
.export(ExportMode::shallow_snapshot(&current_frontiers))
395+
.unwrap();
396+
let shallow_doc = LoroDoc::new();
397+
shallow_doc.import(&shallow_snapshot).unwrap();
398+
399+
let err = shallow_doc.checkout(&pre_shallow_frontiers).unwrap_err();
400+
assert_eq!(err, LoroError::SwitchToVersionBeforeShallowRoot);
401+
assert!(!shallow_doc.is_detached());
402+
assert_eq!(shallow_doc.get_text("t").to_string(), "hello world");
403+
404+
shallow_doc.get_text("t").insert(11, "!").unwrap();
405+
shallow_doc.commit();
406+
assert_eq!(shallow_doc.get_text("t").to_string(), "hello world!");
407+
}
408+
347409
#[test]
348410
fn fix_get_unknown_cursor_position() {
349411
let doc = LoroDoc::new();

0 commit comments

Comments
 (0)