Skip to content

Commit 96e6062

Browse files
committed
fix: handle pre-shallow frontier errors in diff and checkout
1 parent 1e986f3 commit 96e6062

File tree

2 files changed

+93
-15
lines changed

2 files changed

+93
-15
lines changed

crates/loro-internal/src/loro.rs

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

10201027
let (options, txn) = self.implicit_commit_then_stop();
@@ -1351,18 +1358,27 @@ impl LoroDoc {
13511358
/// This will make the current [DocState] detached from the latest version of [OpLog].
13521359
/// Any further import will not be reflected on the [DocState], until user call [LoroDoc::attach()]
13531360
pub fn checkout(&self, frontiers: &Frontiers) -> LoroResult<()> {
1361+
let was_detached = self.is_detached();
13541362
let (options, guard) = self.implicit_commit_then_stop();
1355-
self._checkout_without_emitting(frontiers, true, true)?;
1356-
self.emit_events();
1363+
let result = self._checkout_without_emitting(frontiers, true, true);
1364+
if result.is_ok() {
1365+
self.emit_events();
1366+
}
13571367
drop(guard);
13581368
if self.config.detached_editing() {
1359-
self.renew_peer_id();
1369+
if result.is_ok() {
1370+
self.renew_peer_id();
1371+
}
13601372
self.renew_txn_if_auto_commit(options);
1373+
} else if result.is_err() {
1374+
if !was_detached {
1375+
self.renew_txn_if_auto_commit(options);
1376+
}
13611377
} else if !self.is_detached() {
13621378
self.renew_txn_if_auto_commit(options);
13631379
}
13641380

1365-
Ok(())
1381+
result
13661382
}
13671383

13681384
/// 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)