Sessions and commands
An EditorSession owns the current state. Every change runs through update or dispatch and ends in one commit, or in a value that says why not.
Transactions
A private draft
update hands you a transaction. It latches its first failure and rejects everything after, and its handle is only valid inside the closure.
scala
session.update { tx =>
tx.spliceText(NodeId("t1"), 5, 0, "!")
tx.select(RangeSelection.caret(Point.textAfter(NodeId("t1"), 6)))
} // Either[UpdateError, Commit]
Commands
Intent with a typed payload
A command is an EditorCommand[A], found by reference, never by string. Handlers run from Critical down to Fallback; the first that handles it ends the chain.
scala
session.dispatch(RichText.InsertText, "Hello")
session.dispatch(RichText.InsertParagraph)
session.dispatch(RichText.ToggleMark, StandardMarks.Strong)
session.dispatch(RichText.SetHeading, Some(HeadingLevel.H2))
Commits
Two revisions
revision grows with every publish, documentRevision only when the document changed. Code that persists compares the second, so a moved caret never triggers a write.
scala
var saved = session.state.documentRevision
val subscription = session.onCommit { commit =>
if commit.current.documentRevision != saved then
save(commit.current.document)
saved = commit.current.documentRevision
}
Rules of the boundary
A commit is not a render: whether a view shows it is DocumentView.onProjected's question.
update inside update returns UpdateError.NestedUpdate; use enqueueUpdate from listeners.
Expected failures are Either values; misuse such as a stale transaction handle throws.
A state field's reducer may reject a commit before it happens.