docs/solutions/logic-errors/2026-04-03-slate-history-capture-must-anchor-to-commit-subscribers-not-onchange-order.md
slate-history-v2 looked transaction-aware at first glance, but the first architect review found a real seam leak.
History batches were being derived in a subscribe(...) listener that ran after editor.onChange(). That meant a reentrant onChange() edit could smear or misattribute history units even though the stack was supposed to be anchored to committed transactions.
The core publish order in core.ts was wrong for history capture.
The sequence was:
editor.onChange()Editor.subscribe(...) listenersThat ordering left history downstream of app callbacks instead of at the commit seam itself.
As soon as slate-history-v2 derived batches from editor.operations and previousSnapshot inside a subscriber, the proof depended on callback ordering luck rather than committed transaction boundaries.
Move Editor.subscribe(...) notification ahead of editor.onChange() in slate-v2.
The fix lives in core.ts.
Before:
state.snapshot = snapshot
state.transaction = null
;(editor as MutableEditor).operations = transaction.operations.slice()
syncPublicEditor(editor, snapshot)
editor.onChange()
for (const listener of state.listeners) {
listener(snapshot)
}
After:
state.snapshot = snapshot
state.transaction = null
;(editor as MutableEditor).operations = transaction.operations.slice()
syncPublicEditor(editor, snapshot)
for (const listener of state.listeners) {
listener(snapshot)
}
editor.onChange()
With that ordering in place, slate-history-v2 can safely capture:
previousSnapshoteditor.operationswithout treating app-level onChange() side effects as part of the same history unit.
History is not an app callback concern. It is a commit concern.
Once the committed snapshot exists, the earliest trustworthy place to derive a history batch is the commit subscriber boundary. That boundary still sees:
Calling editor.onChange() afterward keeps userland notification intact, but removes it from the authority chain for history capture.
That gives slate-history-v2 the one thing it actually needs: a stable, pre-userland view of each committed transaction.
editor.onChange() as userland notification, not as a reliable source of commit metadata.onChange() reenters the editor, does this subsystem still capture the original commit correctly?”