Guide

State Topology and History

Read active geometry, state-owned facets, and recorded history precisely.

State topology determines which configurations can exist and which transitions may change them. Data follows that geometry through state-owned facets. History records selected geometry and facets for a later restoration; it is not an implicit persistence rule for every re-entry.

Treat the root as a state

The authored root is the chart and is configured at path "". It may be atomic, compound, or parallel. It may own data, transitions, actions, timers, activities, and children just like any descendant.

Descendant state identity is a dot-joined path. The path records containment, so sibling tags need be unique only among siblings. A Failed state in one parallel region is distinct from a Failed state in another because their paths differ.

Four state kinds determine ordinary geometry:

  • An atomic state has no active descendants.
  • A compound state has one active direct child and that child's active descendants.
  • A parallel state has an active configuration in every direct region.
  • A final state is a leaf that completes its parent and has no outgoing behavior.

History nodes describe routing, not liveness. They never appear in the active configuration and do not count as parallel regions.

Atomic roots have no initial target. For a compound root or state, the first direct child in authored order is the default initial. The optional initial setting exists only to name deeper descendant geometry; naming a direct child there is rejected as InitialOfDirectChildIsSpelledByChildOrder. A parallel state enters every region, and each compound region follows the same child-order law. Initial geometry is chart meaning and is never inferred from a later runtime condition.

Read a configuration as a closed geometric fact

A valid running configuration contains the root, every active leaf, and every ancestor connecting those leaves to the root. It is never the empty set while the instance is running.

For a compound state, the configuration contains exactly one direct child line. For a parallel state, it contains a line in every region. History nodes are excluded even when a transition just used one to choose entry.

Configuration order is authored document order. This order also governs state entry. Exit uses the reverse order, which places descendants before ancestors and reverses sibling order. Stable order makes traces, snapshots, and durable recovery comparable without relying on object enumeration or runtime timing.

A transition's targets determine an exit domain and an entry set. A transition may leave one leaf, a compound branch, several parallel regions, or the whole chart. Targeting the root is an ordinary external re-entry through the boundary above the root: the current configuration exits, root data is reconstructed when the root owns a data slot, and the root's initial geometry enters again in one macrostep.

An internal transition is narrower. It is valid only from a compound source to proper descendants of that source, and it preserves the source while changing the covered descendant line. Invalid internal geometry is rejected at the authoring or chart-validation boundary rather than silently treated as a different transition kind.

Exit before transition before entry

Every microstep follows one geometric phase order. The interpreter first computes the entire exit set. It records history owned by states in that set before any exit action runs. It then runs exit actions in reverse document order and removes the exited states.

Transition actions run after exit. The interpreter then computes the complete entry set, restores history where targeted, and enters states in document order. Parent entry precedes descendant entry.

This phase separation gives producers a stable reading boundary. Exit actions still see facets owned by exiting states. Transition actions can see the post-exit-update value of their source. A target facet is available before that target's entry actions execute.

Mount lifecycles follow the same geometry. Entering a state arms its timers, activities, and children after entry behavior. Exiting that state cancels or stops its mounted work after exit behavior. Re-entry is therefore a new incarnation, not continued liveness under the same state path.

Let each state own its facet

A state with a data slot owns one facet while active. The root facet is stored in the snapshot's data field. Descendant facets are stored by state path. The root is not duplicated in the descendant facet map.

On entry, a transition-supplied value for the target wins. Otherwise the state's constructor supplies the facet. Construction follows entry order, so a parent's facet exists before child entry behavior runs.

On exit, actions run before the facet is dropped. A later ordinary re-entry constructs the facet fresh. State data does not silently persist merely because the same path becomes active again.

Updates merge declared fields into the facet of an active state. Updating an inactive state is a defect because there is no live facet authority at that path. This rule keeps data ownership aligned with active geometry instead of turning the snapshot into a loose chart-wide object.

Terminal snapshots retain facet values for the retained terminal configuration as historical record. Retention after terminal does not mean the facets remain live or can accept updates.

Restore only what history recorded

When a state with history children exits, the interpreter records history before exit actions. A shallow history node records the active direct child. A deep history node records the active atomic or final descendants. Records are stored in deterministic entry order.

Facets follow the same depth:

  • shallow history restores the recorded direct-child facet, then enters deeper descendants fresh; and
  • deep history restores the recorded descendant geometry and every facet at that recorded depth.

If no history record exists, the history node follows its authored default transition. That default is one eventless, unguarded route to proper descendants of the history parent. Once a record exists, the record wins; the default path and its entry props are no longer consulted.

Drive each route into Preview, leave it, and compare the re-entry paths: shallow restores the remembered direct child and initializes below it, while deep restores the remembered leaf configuration.

Starting the state history chart…

import { HistoryDefault, State, Statechart, States } from "@motive/motive";

class Editor extends State.Compound<Editor>()("Editor") {
  static states = States.make(() => [Editing, Resume]);
}

class Editing extends State.Atomic<Editing>()("Editing") {}

class Resume extends State.DeepHistory()("Resume") {
  static readonly default = HistoryDefault.make(this, () => Editing);
}

const EditorChart = Statechart.make(Editor);
void EditorChart;

Because recording happens before entry computation, a transition may exit a parent and target that parent's history in the same microstep. The entry uses the configuration just recorded by that exit.

History under parallel geometry follows the same authority. Deep history can restore the remembered concurrent configuration. Shallow history directly under a parallel state is rejected because it cannot express a meaningful regional restoration.

Keep geometry and record distinct

Active configuration is a liveness claim. Recorded history is a possible future entry route. Retained terminal configuration is evidence of where the instance ended. Those three structures may contain related paths, but only the first names states that are currently active.

That distinction prevents two common errors: treating history as if its states were still mounted, and treating a terminal snapshot as if its retained final configuration could process another event. Geometry determines current authority; records preserve evidence for the boundary that consumes them.