One confirmation, one committed result
Readers never see half a decision.
One event can change geometry, data, and lifecycle boundaries. The chart orders that work internally and publishes only the complete decision.
1 · One event changes geometry
Confirmation enters a new fact.
ConfirmOrder carries an order id and moves Reviewing to Confirmed. The first model changes configuration but does not yet record the identity.
import { State, Statechart, States, Transitions } from "@motive/motive";
import { Schema } from "effect";
const ConfirmOrder = Schema.TaggedStruct("ConfirmOrder", {
orderId: Schema.String,
});
class Order extends State.Compound<Order>()("Order") {
static states = States.make(() => [Reviewing, Confirmed]);
}
class Reviewing extends State.Atomic<Reviewing>()("Reviewing") {
static transitions = Transitions.make(this, ({ on }) => [
on(ConfirmOrder, Confirmed),
]);
}
class Confirmed extends State.Atomic<Confirmed>()("Confirmed") {}
export const OrderChart = Statechart.make(Order);
2 · Configuration and data agree
The same edge records the decision.
The transition now updates the root facet as it enters Confirmed. Observers receive the new state and confirmed id together in one committed snapshot.
import { State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ConfirmOrder = Schema.TaggedStruct("ConfirmOrder", {
orderId: Schema.String,
});
export class Order extends State.Compound<Order>()("Order", {
confirmedOrderId: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("")),
),
}) {
static states = States.make(() => [Reviewing, Confirmed]);
}
class Reviewing extends State.Atomic<Reviewing>()("Reviewing") {
static transitions = Transitions.make(this, ({ on }) => [
on(ConfirmOrder, Confirmed).update(Order, ({ event }) => ({
confirmedOrderId: event.orderId,
})),
]);
}
class Confirmed extends State.Atomic<Confirmed>()("Confirmed") {}
export const OrderChart = Statechart.make(Order);
3 · Order inside, atomic outside
Exit, edge, and entry fold before publication.
Three ordered actions append to the audit trail. The engine computes exit, transition, and entry in sequence, then publishes only their complete result.
import { Query, State, Statechart, States, Transitions } from "@motive/motive";
import * as Effect from "effect/Effect";
import { Schema } from "effect";
const ConfirmOrder = Schema.TaggedStruct("ConfirmOrder", {
orderId: Schema.String,
});
const recordExit = Query.gen(function* () {
const order = yield* Order;
return { audit: [...order.audit, "exit Reviewing"] };
});
const recordConfirmation = Query.gen(function* () {
const event = yield* Query.event(ConfirmOrder);
const order = yield* Order;
return {
audit: [...order.audit, "confirm order"],
confirmedOrderId: event.orderId,
};
});
const recordEntry = Query.gen(function* () {
const order = yield* Order;
return { audit: [...order.audit, "enter Confirmed"] };
});
export class Order extends State.Compound<Order>()("Order", {
confirmedOrderId: Schema.String.pipe(
Schema.withConstructorDefault(Effect.succeed("")),
),
audit: Schema.Array(Schema.String).pipe(
Schema.withConstructorDefault(Effect.succeed([])),
),
}) {
static states = States.make(() => [Reviewing, Confirmed]);
}
class Reviewing extends State.Atomic<Reviewing>()("Reviewing") {
static transitions = Transitions.make(this, ({ exit, on }) => [
exit().update(Order, recordExit),
on(ConfirmOrder, Confirmed).update(Order, recordConfirmation),
]);
}
class Confirmed extends State.Atomic<Confirmed>()("Confirmed") {
static transitions = Transitions.make(this, ({ entry }) => [
entry().update(Order, recordEntry),
]);
}
export const OrderChart = Statechart.make(Order);
One event proposes one decision
ConfirmOrder carries the identity of the order being confirmed. In the first frame, the event
moves the chart from Reviewing to Confirmed.
The second frame records that identity in the root facet on the same edge. Configuration and data are not two loosely coordinated writes; they are parts of one chart decision.
Watch the committed-snapshot count. One send adds one snapshot. There is no published moment where
Confirmed is active with the old id, or Reviewing remains active with the new one.
Order still matters inside
Atomic publication does not erase action order. The final frame makes that internal sequence visible with three updates:
- exiting
Reviewingrecordsexit Reviewing; - the selected edge records
confirm orderand the id; and - entering
Confirmedrecordsenter Confirmed.
Each update reads the result folded so far. The final audit therefore preserves the authored exit–transition–entry order.
Publication is atomic
The engine computes those ordered actions as one macrostep. Only after the fold is complete does it publish the new snapshot.
Readers see either the old world or the new world. The intermediate fold states help compute the decision, but none of them becomes public chart truth.
This boundary will also contain the automatic microsteps introduced next: eventless transitions, raised events, completion, and later mounted outcomes all stabilize before the same publication.
Change the world
Open the active source in the playground and try these changes:
- Add another root field and update it on the confirmation edge.
- Make
Confirmedcarry required target data and construct it fromConfirmOrder. - Reverse two audit labels without moving their actions, then predict which order the runtime displays.