Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/declarative-machine-definition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@typeonce/effect-machine": minor
"@typeonce/effect-machine-react": minor
"@typeonce/effect-machine-devtools": minor
"@typeonce/oxlint-plugin-effect-machine": minor
---

Declare transitions as objects using references from `Machine.targets(Root)`. Replace event-local selector chains with `{ target: targets.root.Ready, from: ({ event }) => ({ value: event.value }) }`, and use `update` for retained state data. `initial`, `history`, `none: true`, `guard`, and `reenter` express their respective operations explicitly.

Register `effects`, `streams`, `timers`, `logic`, and `children` inside `Machine.make`, then invoke them with `{ src, input?, onDone?, onFailure?, onElement?, onSnapshot? }`. Required inputs and reachable outcome handlers are checked from the source types; unused sources do not add service requirements. Pass lazy Effect and Stream values directly, or use a function with one required input and provide an input mapper.

Declare named `branches` in `make` for conditional outcomes, queued commands, or nested construction. A transition uses `{ branches: "group", resolve }`; its `select` constructors come from the declared destinations. Ordinary transitions stay inline. Full root configuration construction remains available at initialization and history fallback; runtime transitions use explicit destinations and retained owner updates. Devtools, testing, React integrations, and lint rules follow the new declarations.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ const Root = Machine.state({
initial: "Locked",
states: { Locked: {}, Unlocked: {} }
})
const targets = Machine.targets(Root)
const Events = Machine.events({ Coin: {}, Push: {} })

const Turnstile = Machine.make({ root: Root, events: Events }).handle({
states: {
Locked: { on: { Coin: (to) => to.local.Unlocked() } },
Unlocked: { on: { Push: (to) => to.local.Locked() } }
Locked: { on: { Coin: { target: targets.root.Unlocked } } },
Unlocked: { on: { Push: { target: targets.root.Locked } } }
}
})

Expand Down
208 changes: 160 additions & 48 deletions api-reference.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,39 @@
"title": "Define state topology",
"description": "Declare the complete state tree and its schema-backed values before constructing the machine.",
"entries": [
{ "declaration": "state" }
{
"declaration": "state"
}
]
},
{
"title": "Define event protocols",
"description": "Describe public input, machine-local events, outward notifications, and parent ownership.",
"entries": [
{ "declaration": "events" },
{ "declaration": "internalEvents" },
{ "declaration": "emittedEvents" },
{ "declaration": "parent" },
{ "declaration": "optionalParent" }
{
"declaration": "events"
},
{
"declaration": "internalEvents"
},
{
"declaration": "emittedEvents"
},
{
"declaration": "parent"
},
{
"declaration": "optionalParent"
}
]
},
{
"title": "Create the definition",
"description": "Combine topology and protocols into a reusable machine definition with an explicit initial target.",
"entries": [
{ "declaration": "make" }
{
"declaration": "make"
}
]
},
{
Expand All @@ -75,9 +89,15 @@
"title": "Run and observe",
"description": "Start or resume an executable machine and subscribe to its state over time.",
"entries": [
{ "declaration": "start" },
{ "declaration": "resume" },
{ "declaration": "watch" }
{
"declaration": "start"
},
{
"declaration": "resume"
},
{
"declaration": "watch"
}
]
}
],
Expand All @@ -87,21 +107,46 @@
"title": "State node configuration",
"description": "Properties accepted by root and nested `Machine.state` descriptors.",
"roots": [
{ "reflection": "Machine.AtomicStateNodeConfig", "label": "Atomic and final states" },
{ "reflection": "Machine.CompoundStateNodeConfig", "label": "Compound states" },
{ "reflection": "Machine.ParallelStateNodeConfig", "label": "Parallel states" },
{ "reflection": "Machine.HistoryStateNodeConfig", "label": "History states" },
{ "reflection": "Machine.ChoiceStateNodeConfig", "label": "Choice states" },
{ "reflection": "Machine.StateNodeAnnotations", "label": "Annotations" }
{
"reflection": "Machine.AtomicStateNodeConfig",
"label": "Atomic and final states"
},
{
"reflection": "Machine.CompoundStateNodeConfig",
"label": "Compound states"
},
{
"reflection": "Machine.ParallelStateNodeConfig",
"label": "Parallel states"
},
{
"reflection": "Machine.HistoryStateNodeConfig",
"label": "History states"
},
{
"reflection": "Machine.ChoiceStateNodeConfig",
"label": "Choice states"
},
{
"reflection": "Machine.StateNodeAnnotations",
"label": "Annotations"
}
]
},
{
"owner": "make",
"title": "Machine definition configuration",
"description": "Parameters accepted by `Machine.make` and its initial-state declaration.",
"roots": [
{ "declaration": "make", "parameter": "config", "label": "Configuration" },
{ "reflection": "Machine.InitialTransitionTarget", "label": "Initial target resolution" }
{
"declaration": "make",
"parameter": "config",
"label": "Configuration"
},
{
"reflection": "Machine.InitialTransitionTarget",
"label": "Initial target resolution"
}
]
},
{
Expand All @@ -110,12 +155,14 @@
"title": "State handler configuration",
"description": "Properties accepted for each state inside `Definition.handle`.",
"roots": [
{ "reflection": "Machine.ActiveStateConfig", "label": "Active states" },
{ "reflection": "Machine.FinalStateConfig", "label": "Final states" },
{ "reflection": "Machine.ChoiceStateConfig", "label": "Choice states", "members": ["choice"] },
{ "reflection": "Machine.HandlerNode", "label": "Nested handler tree", "members": ["states", "history"] },
{ "reflection": "Machine.OutputHandlerConfig", "label": "Output functions", "members": ["output"] },
{ "reflection": "Machine.HistoryDefaultEntry", "label": "History defaults" }
{
"reflection": "Machine.StateHandler",
"label": "State handlers"
},
{
"reflection": "Machine.HistoryDefaultEntry",
"label": "History defaults"
}
]
},
{
Expand All @@ -124,13 +171,26 @@
"title": "Transition declarations",
"description": "Target selection, resolution, branching, and commands available to state and invocation transitions.",
"roots": [
{ "reflection": "Machine.TargetSelector", "label": "Target selection" },
{ "reflection": "Machine.TargetBuilder", "label": "Resolver target construction" },
{ "reflection": "Machine.TransitionSelector", "label": "Named branch selection", "nested": true },
{ "reflection": "Machine.TransitionTarget", "label": "Selected target" },
{ "reflection": "Machine.TransitionBranchInput", "label": "Named branches" },
{ "reflection": "Machine.DeclineCapability", "label": "Declinable transitions" },
{ "reflection": "Enqueue", "label": "Enqueued commands" }
{
"reflection": "Targets",
"label": "State references"
},
{
"reflection": "Machine.Transition",
"label": "Inline transitions"
},
{
"reflection": "Machine.BranchDeclaration",
"label": "Named branch declarations"
},
{
"reflection": "Machine.DeclineCapability",
"label": "Declinable transitions"
},
{
"reflection": "Enqueue",
"label": "Enqueued commands"
}
]
},
{
Expand All @@ -139,14 +199,38 @@
"title": "Handler and resolver contexts",
"description": "Values supplied to state actions, transitions, completion handlers, choices, and output functions.",
"roots": [
{ "reflection": "MachineReferences", "label": "Machine references" },
{ "reflection": "Machine.HandlerContext", "label": "Event handlers" },
{ "reflection": "Machine.StateActionContext", "label": "Entry and exit actions" },
{ "reflection": "Machine.AlwaysContext", "label": "Eventless transitions" },
{ "reflection": "Machine.DoneContext", "label": "State completion" },
{ "reflection": "Machine.ChoiceContext", "label": "Choice resolution" },
{ "reflection": "Machine.FinalOutputContext", "label": "Final output" },
{ "reflection": "Machine.ParallelOutputContext", "label": "Parallel output" }
{
"reflection": "MachineReferences",
"label": "Machine references"
},
{
"reflection": "Machine.HandlerContext",
"label": "Event handlers"
},
{
"reflection": "Machine.StateActionContext",
"label": "Entry and exit actions"
},
{
"reflection": "Machine.AlwaysContext",
"label": "Eventless transitions"
},
{
"reflection": "Machine.DoneContext",
"label": "State completion"
},
{
"reflection": "Machine.ChoiceContext",
"label": "Choice resolution"
},
{
"reflection": "Machine.FinalOutputContext",
"label": "Final output"
},
{
"reflection": "Machine.ParallelOutputContext",
"label": "Parallel output"
}
]
},
{
Expand All @@ -155,13 +239,34 @@
"title": "State invocation",
"description": "State-owned Effect, Stream, timer, logic, and child lifecycles declared through `invoke`.",
"roots": [
{ "reflection": "Machine.InvokeSelector", "label": "Invocation sources" },
{ "reflection": "Machine.InvokeBuilder", "label": "Lifecycle handlers" },
{ "reflection": "Machine.InvokeContext", "label": "Source context" },
{ "reflection": "Machine.InvokeDoneContext", "label": "Completion context" },
{ "reflection": "Machine.InvokeFailureContext", "label": "Failure context" },
{ "reflection": "Machine.InvokeElementContext", "label": "Stream element context" },
{ "reflection": "Machine.InvokeSnapshotContext", "label": "Child snapshot context" }
{
"reflection": "Machine.Invocation",
"label": "Invocation configuration"
},
{
"reflection": "Machine.Registrations",
"label": "Registered sources"
},
{
"reflection": "Machine.InvokeContext",
"label": "Input mapper context"
},
{
"reflection": "Machine.InvokeDoneContext",
"label": "Completion context"
},
{
"reflection": "Machine.InvokeFailureContext",
"label": "Failure context"
},
{
"reflection": "Machine.InvokeElementContext",
"label": "Stream element context"
},
{
"reflection": "Machine.InvokeSnapshotContext",
"label": "Child snapshot context"
}
]
}
],
Expand Down Expand Up @@ -196,13 +301,20 @@
"export": "./unstable/reactivity/AtomMachine",
"source": "src/unstable/reactivity/AtomMachine.ts",
"barrel": "./reactivity",
"examples": ["make", "matches", "select", "selectChild"]
"examples": [
"make",
"matches",
"select",
"selectChild"
]
},
{
"export": "./unstable/cluster/ClusterMachine",
"source": "src/unstable/cluster/ClusterMachine.ts",
"barrel": "./cluster",
"examples": ["make"]
"examples": [
"make"
]
}
]
}
39 changes: 24 additions & 15 deletions packages/devtools/src/internal/browser/example-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,14 @@ const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining<
"application.workflow"
> => snapshot

const targets1 = Machine.targets(States)
export const machine = Machine.make({
branches: {
transition1: {
destination: { target: targets1.root.application.workflow.running, update: targets1.root.application.workflow }
}
},

id: "inspection-example",
root: States,
events: Machine.eventsFromSchemas(Start, Finish, Disconnect, Refresh),
Expand All @@ -110,27 +117,29 @@ export const machine = Machine.make({
states: {
idle: {
on: {
Start: (to) =>
to.local.running()
.updating(to.branch.application.workflow)
.resolve(({ owner, target }) =>
target.decoded(
new Running({}),
(running) => running.editing.decoded(new Editing({}))
).update(owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 3 })))
),
Refresh: (to) =>
to.local.update.resolve(({ owner }) =>
owner.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
)
Start: {
branches: "transition1",
resolve: ({ select: { destination: target } }) =>
target.decoded(
new Running({}),
(running) => running.editing.decoded(new Editing({}))
).update.decoded(new Workflow({ document: "Machine.ts", unsavedChanges: 3 }))
},
Refresh: {
update: targets1.root.application.workflow,
decoded: () => (new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
}
}
},
running: {
initialize: ({ builder }) => builder.decoded(new Editing({})),
states: {
editing: {
on: {
Finish: (to) => to.local.complete().resolve(({ target }) => target.decoded(new Complete({})))
Finish: {
target: targets1.root.application.workflow.running.complete,
decoded: () => (new Complete({}))
}
}
}
}
Expand All @@ -141,7 +150,7 @@ export const machine = Machine.make({
states: {
online: {
on: {
Disconnect: (to) => to.local.offline().resolve(({ target }) => target.decoded(new Offline({})))
Disconnect: { target: targets1.root.application.connection.offline, decoded: () => (new Offline({})) }
}
}
}
Expand Down
Loading