docs/solutions/logic-errors/2026-04-07-slate-v2-node-op-wrappers-must-not-reuse-runtime-ids-or-read-committed-snapshots-inside-transactions.md
The first core op-family slice added real insert_node / remove_node
operations and matching Transforms.insertNodes(...) /
Transforms.removeNodes(...) wrappers to the replacement-candidate slate
package.
Two easy mistakes showed up immediately:
removeNodes(...) tried to read from the committed snapshot while an outer
transaction was still openBoth bugs looked harmless at first. Neither was.
insert_node initially built its draft node with the current tree's lookup
index.
That reused the runtime id from the node already sitting at the insertion path.
The snapshot test caught it right away: after inserting before alpha, the new
node at path [0] inherited alpha's runtime id instead of getting a fresh
one.
That breaks the core rule:
If inserted nodes can steal old ids, selector subscriptions and path/id reasoning become garbage.
removeNodes(...) initially tried to resolve the node payload from
Editor.getSnapshot(editor) before dispatching remove_node.
That is wrong inside an outer Editor.withTransaction(...) block.
Editor.getSnapshot(...) is the committed snapshot, not the live draft. So
after a same-transaction insert, resolving path [3] from the committed
snapshot failed even though the draft tree already had that node.
The rule is simple:
The honest fix was narrow:
insert_node now creates the inserted draft subtree without reusing the
existing indexremove_node keeps the payload optional for now, so removeNodes(...) can
dispatch by path without probing the committed snapshotThat keeps the slice honest:
For Slate v2 core node operations:
Editor.getSnapshot(...) to resolve data
that should come from the live draft inside the same transactionIf a wrapper needs live tree data and only the committed snapshot can answer it, the wrapper is wired to the wrong seam.