Engine
The orchestrator that owns committed state.
The pipeline stages — intent, plan, transaction, apply, normalize — are all pure functions. Something has to hold the current EditorState, thread each intent through the stages in order, coordinate history and subscribers, and emit trace events at every boundary. That is EditorEngine.
Worked example
const engine = createEditorEngine({
initialState,
planner: createPlanner(),
tracer: createTracer(new MemoryTraceSink()),
});
engine.subscribe((next, prev) => {
render(next.doc, next.selection);
});
const result: DispatchResult = engine.dispatch({ kind: "typeText", text: "a" });
if (result.applied) {
persist({
doc: result.stateAfter.doc,
transactionJSON: transactionToJSON(result.transaction),
historyEntry: result.historyEntry ?? null,
});
}
The important bit is that dispatch returns both the final state and the artifacts that explain how the state was produced. A no-op still returns stateBefore, stateAfter, and the compiled empty transaction; it omits apply-only artifacts such as mapping, normalization, and historyEntry, and sets stoppedAt: "noOp".
The dispatch sequence
EditorEngine.dispatch (see packages/core/src/engine/index.ts) does, in order:
- Snapshot
stateBefore. - Emit
intentReceivedwith{ intentKind, intentJSON }. - Build a
PlannerContextfromstateBeforeand compile the intent. - Emit
planCompiledwith{ planId, intentKind, opCount }. - Lower the plan to a
Transaction. If the transaction has zero steps, zero selection update, and zero stored-marks update, return{ applied: false, transaction, stoppedAt: "noOp" }— no apply, normalization, history, or subscriber notification fires. applyTransaction(stateBefore.doc, transaction).- Emit
transactionAppliedwith{ transactionId, stepCount }. - Normalize the applied doc. For each rule that reports a change, emit
normalizedwith{ ruleId }. - Commit the result into a new frozen
EditorState. Revalidate structural selections against the new doc. - If
shouldRecordHistoryEntryis true, build aHistoryEntryand push-or-coalesce it. Emithistorywith{ action: "push" }. - Notify every subscriber with
(nextState, previousState).
undo() and redo() replay the stored history entries, emit history with { action: "undo" } or { action: "redo" }, and notify subscribers — they do not go through dispatch.
EditorState
EditorState is the single source of truth. Fields include doc, selection, storedMarks, storedMarksExplicit, and the resolved plugin state. Every commit produces a new frozen state; the previous reference stays valid for subscribers that need a diff.
API
createEditorEngine(config: EditorEngineConfig): EditorEngine;
class EditorEngine {
readonly state: EditorState;
readonly history: HistoryState;
readonly plugins: PluginResolution;
readonly commands: CommandRegistry;
dispatch(intent: Intent): DispatchResult;
dispatchCommand(id: CommandId, args?: JsonValue): DispatchResult | null;
undo(): UndoResult;
redo(): RedoResult;
canUndo(): boolean;
canRedo(): boolean;
clearHistory(): void;
subscribe(subscriber: StateSubscriber): SubscriberId;
unsubscribe(id: SubscriberId): boolean;
}
EditorEngineConfig requires initialState and planner. It can also accept plugins, history, tracer, normalizer, commandRegistry, and historyGroupingPolicy.
undo() and redo() throw when there is nothing to undo or redo; guard with canUndo() / canRedo().