Repository navigation
fix(loro.js): revive a List child that a replay-fallback event re-inserts - #1150
Merged
Merged
Conversation
A child container without ops in the range was emitted only as a reference, so applyDiff/revertTo recreated it empty. Emit its full state, as Rust does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le in diff() Emit a child's whole state only when it is unreachable at `from`, replace a partial diff for such a child, include revived tree node metadata, and order the result parent-first. Moved movable-list children and mergeable children keep only their own ops. applyDiff now moves an existing movable-list child instead of recreating it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ved-child-content
…ve tree subtrees in diff() Port Rust's MovableListHandler::apply_delta so a moved child becomes one move instead of reordering the whole list, undo movable-list moves through their recorded neighbors, and emit creates for every node that becomes reachable, including descendants of revived nodes and nodes moved out of a deleted ancestor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oot in diff() On a shallow doc a node created before the root has no retained op at the root version; use its root-state placement instead of treating it as revived. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ved-child-content
…peated moves, and restore mergeable markers on undo applyDiff now finds moved children through their inserted ids and places every inserted item after its final-order predecessor, so unmerged delete items, several children moved out of one range, and scalar-only deletes are handled without index bookkeeping or scans. Undo reverts earlier moves of an element that only this document moved since. A wide revived subtree is walked iteratively. Undoing a mergeable key deletion re-ensures the marker. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… subtree in diff() A node alive at `from` inside a subtree revived by the range (e.g. a child moved out of a deleted parent and then reverted back under it) was created again while its old copy stayed. Delete the topmost such nodes first, as Rust's diff does, then create the subtree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…delta validation, and linear tree revival - Undo only lets later moves the UndoManager tracks (its recorded edits and the inverses it wrote) pass; excluded-origin and remote moves block. - Each move records its physical successor; undo puts the element back there, so redo after mixed moves and inserts restores the original order. - applyDiff validates list deltas for the whole batch before applying, hydrates lazily imported lists before looking for moved children, and deletes only the gaps between kept positions. - Tree revival memoizes "alive at from" along ancestor paths and emits the deletes of moved-in live nodes first, with their index at `from`. - Undo restores a mergeable marker over a later overwrite. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ved-child-content
…event
When a checkout or import attached a child container again (a map key
set back to an older text, a restored list element), the child's event
was missing or was a delta against the state it had while hidden. A
listener starts an attached child from empty, so it ended up with wrong
content, or with an invalid delta ("text span beyond 0").
Like Rust's DocState::apply_diff, revive every child that a batch
attaches: its event becomes its whole state, recursively, and events are
ordered parent first. A movable-list child that only moved keeps its
delta. Uses #attachedChildren and #reachableThroughParentAt from the
diff() fix this branch is based on.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ved-child-content # Conflicts: # loro-js/src/runtime/document.ts
…not moved since The old successor may have been moved by an untracked op; prefer the old predecessor, then the successor, and write no op when the element is already there. Matches Rust in 70 of 72 neighbor-change cases (the other two: Rust revives a deleted element as an empty container). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drop tracked ranges that end before the earliest stacked op of their peer, and all of them on clear/destroy; only later moves are ever checked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in apply order Put the diff's own deletes and the moved-in deletes first, resolve each parent's sibling order at `from` from history (nodes that only exist at `from` included), and count indexes with a Fenwick tree as the deletes are applied. Fixes indexes next to new roots and the quadratic rescans. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Simulate list lengths in batch order before any write: lists created by an earlier entry start empty, repeated entries see the previous result, and the lookup creates nothing (no empty root after a rejected batch). Entries whose bounds were checked skip the second validation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ith Rust #1134 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ved-child-content
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port Pruning visited both stacks on every commit (quadratic sessions), and compared a peer's ranges only with that peer's items, so after a peer change it could drop a range a stacked move still needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore writing Tree diff items now apply in order, as in Rust: moves of live nodes (parent first), then the topmost deletes, then revived subtrees, with every index counted in the tree the earlier items leave. applyDiff applies tree items in order and dry-runs them with the list checks, so a failing batch writes nothing. A node hidden at the target version is emitted as a delete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… time The reachability filter and tree revival checked each node's ancestors without a cache (quadratic in depth). Fractional indexes are now built as flat strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A create of a node that is alive here moves it, a move of a missing or deleted node creates a new one, and a delete of a deleted node does nothing, so default applyDiff on a doc holding a mergeable tree's hidden state no longer duplicates its nodes. A regular map child is always set as a new container, so its full state is not appended to a hidden copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CI runner is several times slower than a dev machine, so the 1.5 s bound flaked; compare 8,000-deep with 2,000-deep instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ved-child-content
…ved-child-content # Conflicts: # loro-js/src/runtime/document.ts
#1131 now revives re-attached children in #emit itself (#completeDiffEntries), so take its version and drop #reviveAttachedChildren. One case is left: its reachability exemption covers every sequence, but a listener only keeps a moved child for a movable list. For events, apply the exemption to movable lists only, so a child that a whole-value List diff (the replay fallback) deletes and re-inserts is revived. diff()/revertTo keep #1131's behavior. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hild-events # Conflicts: # loro-js/src/runtime/document.ts
zxch3n
changed the base branch from
fix/loro-js-diff-revived-child-content
to
main
September 29, 2026 11:15
Contributor
WASM Size Report
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #1131 (merged as
238988f5); now based onmain, and the diff againstmainis the 9-line change below plus the test.Summary
#1131 now makes
#emitreport the whole state of a child container that an import or checkout re-attaches (#completeDiffEntries). That covers most of what this PR originally did, so the original#reviveAttachedChildrenis gone. One case is left.When a checkout falls back to replaying history (for example, after updates were imported while detached), a plain List diff is computed from whole values. It can delete and re-insert an unchanged child container.
#completeDiffEntriesskips children that were already reachable through that slot atfrom. That exemption is meant for movable-list moves, but it applies to every sequence. The event therefore re-inserts the child without its content, and a listener that starts an inserted child from empty (as the Rust fuzzer'sContainerTrackerdoes) loses it:["s", true, "", false]instead of["s", true, "a", false].Change
For events only (
forEvent), the exemption applies toLoroMovableListparents, where a listener keeps the moved child.diff()andrevertTo()keep #1131's behavior. The change is 9 lines indocument.ts.Validation
loro-js/tests/revived-child-events.test.ts: three cases whose events equalloro-crdt's (these now also pass on fix(loro.js): include reattached child containers' state in diff() and revertTo() #1131 alone) and the fallback case above, which fails on fix(loro.js): include reattached child containers' state in diff() and revertTo() #1131's head.pnpm -C loro-js checkwithLORO_REQUIRE_WASM_REFERENCE=1onmain+ this change (543a3f02): 29 files, 483 tests pass.main(238988f5; seeds 297, 315, 344, 422, 452, 559, all event-mirror/event-invalid on a List child), 0 on this branch.🤖 Generated with Claude Code