docs/solutions/runtime-errors/2026-05-23-slate-dom-operation-middleware-must-enter-operation-root-before-path-reads.md
Multi-root history replay could crash when undoing a batch from one root while another root was focused.
The core replay path was already root-aware, but slate-dom operation middleware read model paths before next(op) handed the operation to core apply.
packages/slate-dom/src/plugin/with-dom.ts getMatches.Cannot find a descendant at path [1] in node: {"api":{}}.slate-history selection filters would not fix this. The failing operation already carried the right root.slate-dom would hide the crash while leaving DOM key repair scoped to the wrong root.Wrap the DOM operation middleware body with withOperationRootChildren(e, op, ...).
That scopes all pre-apply and post-apply path reads to the operation root:
apply({ operation: op, next }) {
withOperationRootChildren(e, op, () => {
const matches: [Path, Key][] = []
const pathRefMatches: [PathRef, Key][] = []
// pre-apply path/key collection
// next(op)
// post-apply NODE_TO_KEY repair
})
}
The regression belongs in slate-dom, not only slate, because the bug lives in middleware ordering around DOM key preservation.
const runtime = createEditorRuntime({
extensions: [history(), dom()],
initialValue: {
roots: {
header: [paragraph('header')],
main: [paragraph('first'), paragraph('second')],
},
},
})
mainEditor.update((tx) => {
tx.selection.set({
anchor: { path: [1, 0], offset: 'second'.length },
focus: { path: [1, 0], offset: 'second'.length },
})
tx.text.insert('!')
})
headerEditor.update((tx) => {
tx.history.undo()
})
Operation root ownership is not only a core apply concern.
Any middleware that reads model paths before calling next(op) must enter the same operation root that core apply will use later. Otherwise a valid main operation at [1, 0] can be looked up inside header, where [1] does not exist.
Editor.levels, NodeApi.get, Editor.pathRef, or state.nodes.get using an operation path, wrap that work in the operation root first.