Skip to content

Keep the node index honest across an in-place subtree edit - #17

Merged
ww-mw merged 2 commits into
mainfrom
mutate-subtree
Sep 9, 2026
Merged

Keep the node index honest across an in-place subtree edit#17
ww-mw merged 2 commits into
mainfrom
mutate-subtree

Conversation

@ww-mw

@ww-mw ww-mw commented Sep 9, 2026

Copy link
Copy Markdown
Member

Why

A host that owns its own undo stack — the VS Code custom editor for a
compressed-binary .sldd — does not mutate the tree through deleteNodesById or
the other session forms. It edits nodes in place and then re-registers the source
so nodeIndex describes the result. On a real customer dictionary (75 MB of
data/chunk0.xml, 31k entries) that re-registration is a 3.2 s parse, and the
edit path was paying it twice per committed cell: 7.5 s of host time to express a
one-cell change.

Simply skipping the re-registration is not an option, because it leaves the index
wrong. A node's id is a path, so renaming an entry rekeys the entry and
every descendant
; adding or removing a child adds or strips an id the session
hands out. The edit itself looks fine — it is the next edit on an affected row
that fails to resolve, or routes into a detached node.

What

mutateSubtree(root, mutate) performs the same repair at subtree scope:

  • snapshots root.flatten() ids before the mutation and rebuilds them after,
    so a rename rekeys descendants too;
  • releases the selection for nodes the mutation removed — before minus after
    by identity, so a rename (same nodes, new ids) releases nothing;
  • takes the snapshot inside the wrapper on purpose: id is a live getter, so a
    caller snapshotting after the fact would delete exactly the entries it meant to
    keep;
  • repairs in a finally, so a mutation that throws still leaves the index
    describing the tree as it stands.

Scope is root.flatten() on both sides, matching what indexSource /
deindexSource index for a whole source. No change-notification is published —
the caller that owns the mutation owns telling its own view.

Tests

test/mutateSubtree.test.ts, 10 cases: a rename rekeys the node and its
descendants while the rest of the document stays indexed and the selection
survives; an added child becomes findable; a removed name-keyed child becomes
null; a removed positional child stops being handed out even though a sibling
inherits its id; a selection on a removed node is released; the wrapper returns
the mutation's value; the index is repaired even when the mutation throws.

npm run verify green: 4093 tests, dist rebuilt, check:pack and check:leak OK.

A host that owns its own undo stack does not mutate the tree through the
session's forms: it edits nodes in place and re-registers the source to make
the index describe the result. That re-registration means re-parsing the whole
document, which on a 75 MB dictionary costs seconds per keystroke — but skipping
it leaves the index wrong, because a node's id is a PATH, so renaming an entry
rekeys the entry and everything under it, and adding or removing a child adds or
strips an entry the session hands out.

mutateSubtree does the same repair at subtree scope. It snapshots the ids before
the mutation and rebuilds them after, so a rename rekeys descendants too, and
releases the selection for nodes the mutation removed — the same release
deleteOneNode does, computed by identity so a rename (same nodes, new ids)
releases nothing. The snapshot is taken inside the wrapper because `id` is a
live getter: a caller that snapshotted after the fact would delete exactly the
entries it meant to keep. The repair runs in a `finally` so a mutation that
throws still leaves the index describing the tree as it stands.
@ww-mw
ww-mw merged commit 6b32923 into main Sep 9, 2026
1 check passed
@ww-mw
ww-mw deleted the mutate-subtree branch September 9, 2026 15:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant