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
8 changes: 8 additions & 0 deletions .changeset/calm-machines-construct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@typeonce/effect-machine": minor
---

Add safe `.from` state construction to initial and transition target builders.
Constructor inputs are resolved through the selected state schema during
planning, preserving defaults and class identity while reporting validation
failures as `MachineSchemaDecodeError` values.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ const Counter = Machine.make({
id: "Counter",
states: States.states,
events: [Event.cases.Start],
initial: () => States.initial.Idle(State.cases.Idle.make({}))
initial: () => States.initial.Idle.from({})
}).handle({
Idle: {
on: {
Start: ({ target }) => target.full.Running(State.cases.Running.make({}))
Start: ({ target }) => target.full.Running.from({})
}
},
Running: {}
Expand All @@ -69,6 +69,23 @@ const Counter = Machine.make({
`initial` is always a function. For a machine with an input schema, the
initializer receives the decoded input.

Builder methods accept an already constructed state value directly, or expose
`.from` for constructing one safely from the state schema's make input:

```ts
target.local.Running(decodedRunning)
target.local.Running.from({ startedAt: event.at })
```

Use the direct call when a decoded value already exists. Use `.from` when
entering a state from fields. Construction runs through the schema's
`makeEffect` while the machine plans the configuration, so constructor
defaults and tagged-class identity are preserved and failed refinements become
`MachineSchemaDecodeError` failures instead of synchronous throws. The same
form is available on initial, local, branch, full, compound, parallel, and
final builders. A `.from` builder result is therefore a machine construction
instruction; it becomes a validated public snapshot when planning succeeds.

Tagged classes are equally valid when cases need class methods or nominal
identity:

Expand Down Expand Up @@ -157,8 +174,7 @@ Handlers implement behavior and output computation without repeating it:
const machine = Machine.make({
states: States.states,
events: [],
initial: () =>
States.initial.Form(State.cases.Form.make({ draft: "" }), (form) => form.Editing(State.cases.Editing.make({})))
initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from({}))
}).handle({
Form: {
states: {
Expand Down
16 changes: 16 additions & 0 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,22 @@ Refresh: {
Do not use `target.full` merely because it is easiest to discover. Prefer the
narrowest builder that expresses the intended configuration change.

Every state builder method has two construction forms:

```ts
target.local.Ready(decodedReady)
target.local.Ready.from({ value: event.value })
```

The direct call accepts the schema's decoded `Type`. `.from` accepts its
`~type.make.in`, so callers do not need to invoke a TaggedUnion case's `make`
or instantiate a TaggedClass. The machine resolves `.from` with
`schema.makeEffect` during planning. Constructor defaults and class identity
are retained; refinement failures use `MachineSchemaDecodeError` at the state
boundary rather than throwing synchronously. This applies recursively to
initial, full, local, branch, compound, parallel, final, and `local.with`
builders.

## Reading state and parents

`Machine.defineStates` returns typed helpers:
Expand Down
Loading