On 1.13.7, a detached doc that calls checkout(f) where f is both its current frontiers and HEAD comes back attached — isDetached() returns false. On 1.13.6 the same sequence stays detached.
Repro
import { LoroDoc } from 'loro-crdt';
const doc = new LoroDoc();
const t = doc.getText('t');
const cps = [];
for (const ch of ['a', 'b', 'c']) { t.insert(t.length, ch); doc.commit(); cps.push(doc.frontiers());
}
// scrub forward through every checkpoint, then back
const fwd = cps.map(f => (doc.checkout(f), doc.isDetached()));
const rev = [...cps].reverse().map(f => (doc.checkout(f), doc.isDetached()));
console.log('forward:', fwd, 'reverse:', rev);
1.13.6 forward: [true, true, true] reverse: [true, true, true]
1.13.7 forward: [true, true, true] reverse: [false, true, true]
The differing step is the first reverse one. The forward pass ends on cps[2], which is HEAD, so that call re-checks-out the position the doc already occupies.
Cause
b81abfc (#974, "harden diff calc for shallow text histories") added one line inside the pre-existing no-op early return in loro-internal/src/loro.rs:
+ self.set_detached(frontiers != &self.oplog_frontiers()); return Ok(()); }
Given that #974 mentions splitting shallow-root dependency checks from checkout semantics, this may well be intentional — so I'm asking rather than reporting a straight regression.
The part I'm unsure about
Only the no-op path derives the flag this way; the normal checkout path still sets detached unconditionally. So isDetached() at HEAD now depends on how you got there:
// both end sitting on HEAD, both detached beforehand
doc.checkout(mid); doc.checkout(head); // -> isDetached() === true
doc.checkout(head); // (already there) -> isDetached() === false
Two readings, and I don't know which you intend:
- Detached means "position != HEAD", derived. Then the normal path should agree —
checkout(head) from anywhere ought to report attached — and this is a breaking change worth a changelog line, since "detached while viewing HEAD" stops being
representable.
- Detached stays an explicit mode, entered via checkout and left via checkoutToLatest. Then the no-op path shouldn't derive it.
For what it's worth on (2): on 1.13.6 a doc detached at HEAD could still re-attach normally via checkoutToLatest(), so this doesn't appear to have been fixing an unable-to-re-attach case.
Why I care
We drive a rich-text editor's read-only state off isDetached() while the user scrubs a history timeline. Dragging onto the newest entry when already parked there quietly makes the editor writable while the user is still in history view. It broke a scrubber regression test that walks the timeline forwards then backwards, which is how I found it.
Mostly I'd like to know which behavior to code against going forward. Happy to test a patch.
Environment
loro-crdt 1.13.7 (npm), Node 22, Linux. Same script on 1.13.6 for the comparison.
On 1.13.7, a detached doc that calls
checkout(f)wherefis both its current frontiers and HEAD comes back attached —isDetached()returns false. On 1.13.6 the same sequence stays detached.Repro
The differing step is the first reverse one. The forward pass ends on
cps[2], which is HEAD, so that call re-checks-out the position the doc already occupies.Cause
b81abfc (#974, "harden diff calc for shallow text histories") added one line inside the pre-existing no-op early return in
loro-internal/src/loro.rs:+ self.set_detached(frontiers != &self.oplog_frontiers()); return Ok(()); }Given that #974 mentions splitting shallow-root dependency checks from checkout semantics, this may well be intentional — so I'm asking rather than reporting a straight regression.
The part I'm unsure about
Only the no-op path derives the flag this way; the normal checkout path still sets detached unconditionally. So
isDetached()at HEAD now depends on how you got there:Two readings, and I don't know which you intend:
checkout(head)from anywhere ought to report attached — and this is a breaking change worth a changelog line, since "detached while viewing HEAD" stops beingrepresentable.
For what it's worth on (2): on 1.13.6 a doc detached at HEAD could still re-attach normally via
checkoutToLatest(), so this doesn't appear to have been fixing an unable-to-re-attach case.Why I care
We drive a rich-text editor's read-only state off
isDetached()while the user scrubs a history timeline. Dragging onto the newest entry when already parked there quietly makes the editor writable while the user is still in history view. It broke a scrubber regression test that walks the timeline forwards then backwards, which is how I found it.Mostly I'd like to know which behavior to code against going forward. Happy to test a patch.
Environment
loro-crdt 1.13.7 (npm), Node 22, Linux. Same script on 1.13.6 for the comparison.