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
9 changes: 9 additions & 0 deletions .changeset/fresh-root-object-construction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@typeonce/effect-machine": minor
---

Pass machine input directly to root initial constructors, including parallel regions, without retaining startup-only values in root data. A root transition uses `{ target: targets.root, input }` to reconstruct root and its initial children with fresh input; `reenter: true` restarts the root lifecycle when the handler is at root. Root updates continue to use `{ update: targets.root, data }` and retain active children.

Use `target` instead of `initial` on event transitions. Named branch selectors now accept one construction object: `select.checkout({ data: cart, states: { Review: { data: review } } })`. Replace `.from(value)` with `({ data: value })`, `.decoded(value)` with `({ decoded: true, data: value })`, and chained owner updates with `update: { data: owner }` inside the same call. History fallbacks use `target({ states: ... })` with a complete tree containing their owner.

Input remains limited to root construction and the root's initial callbacks. Nested initializers use state data and ancestors. Explicit subtree construction preserves source-local parallel retention, schema validation, and declared branch inspection.
8 changes: 6 additions & 2 deletions packages/devtools/src/internal/browser/example-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,12 @@ export const machine = Machine.make({
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 }))
target({
data: new Running({}),
decoded: true,
states: { editing: { data: new Editing({}), decoded: true } },
update: { data: new Workflow({ document: "Machine.ts", unsavedChanges: 3 }), decoded: true }
})
},
Refresh: {
update: targets1.root.application.workflow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export const hierarchyRoutingMachine = Machine.make({
branches: "transition1",
resolve: ({ event, select }) =>
event.route === "save"
? select.save.from()
: select.invalid.from({ message: "Add a title before continuing." })
? select.save({})
: select.invalid({ data: { message: "Add a title before continuing." } })
}
},
states: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export const invokeOutcomesMachine = Machine.make({
branches: "transition13",
resolve: ({ select, snapshot }) =>
snapshot.state === "ready"
? select.ready.decoded(new Completed({ source: "process", result: "ready" }))
? select.ready({ data: new Completed({ source: "process", result: "ready" }), decoded: true })
: select.waiting()
}
}
Expand All @@ -279,12 +279,12 @@ export const invokeOutcomesMachine = Machine.make({
},
Completed: {
on: {
Reset: { initial: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
Reset: { target: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
}
},
Failed: {
on: {
Reset: { initial: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
Reset: { target: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ export const layoutResilienceMachine = Machine.make({
branches: "transition4",
resolve: ({ event, select }) => {
if (event.route === "login") {
return select.login.from()
return select.login({})
}
if (event.route === "verification") {
return select.requestVerification.from()
return select.requestVerification({})
}
return select.invalid.from({ message: "Enter valid authentication details." })
return select.invalid({ data: { message: "Enter valid authentication details." } })
}
}
},
Expand All @@ -149,7 +149,7 @@ export const layoutResilienceMachine = Machine.make({
id: "request-verification",
input: (context) => context,
onDone: {
initial: targets1.root.Verification,
target: targets1.root.Verification,
data: ({ containingState }) => ({
mode: containingState.mode,
email: containingState.email,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const parallelCompletionMachine = Machine.make({
Cart: {
on: {
Checkout: {
initial: targets1.root.Order,
target: targets1.root.Order,
decoded: true,
data: ({ event }) => (new Order({ orderId: event.orderId, total: event.total }))
}
Expand Down Expand Up @@ -242,7 +242,7 @@ export const parallelCompletionMachine = Machine.make({
Cancelled: {
on: {
RetryOrder: {
initial: targets1.root.Order,
target: targets1.root.Order,
decoded: true,
data: () => (new Order({ orderId: "retry", total: 0 }))
}
Expand Down
4 changes: 2 additions & 2 deletions packages/devtools/src/internal/browser/planner-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ export const plannerMachine = Machine.make({
}
const working = new Working({ owner: state.owner, job: event.job })
return event.priority === "urgent"
? select.urgent.decoded(working)
: select.normal.decoded(working)
? select.urgent({ data: working, decoded: true })
: select.normal({ data: working, decoded: true })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const requiredParentChildMachine = Machine.make({
resolve: ({ event, select: { destination: target } }, enqueue) => {
enqueue.raise(ChildInternalEvents.Heartbeat({ percent: 25 }))
enqueue.emit(ChildEmissions.ChildTrace({ message: `started ${event.job}` }))
return target.decoded(new ChildWorking({ job: event.job, progress: 0 }))
return target({ data: new ChildWorking({ job: event.job, progress: 0 }), decoded: true })
}
}
}
Expand All @@ -111,7 +111,7 @@ export const requiredParentChildMachine = Machine.make({
branches: "transition3",
resolve: ({ event, state, select: { destination: target } }, enqueue) => {
enqueue.raise(ChildInternalEvents.CommitChildWork())
return target.decoded(new ChildWorking({ job: state.job, progress: event.percent }))
return target({ data: new ChildWorking({ job: state.job, progress: event.percent }), decoded: true })
}
},
CommitChildWork: {
Expand Down Expand Up @@ -263,7 +263,7 @@ export const optionalParentMachine = Machine.make({
if (parent !== undefined) {
enqueue.sendTo(parent, ParentEvents.ChildFinished({ result: event.result }))
}
return target.decoded(new Published({ deliveredToParent: parent !== undefined }))
return target({ data: new Published({ deliveredToParent: parent !== undefined }), decoded: true })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ export const transitionSemanticsMachine = Machine.make({
branches: {
transition4: {
draft: { target: targets1.root.Workspace.Draft, title: "Preferred route is draft" },
review: { initial: targets1.root.Workspace.Review, title: "Preferred route is review" }
review: { target: targets1.root.Workspace.Review, title: "Preferred route is review" }
},
transition7: {
review: { initial: targets1.root.Workspace.Review, title: "Enter the review flow" },
review: { target: targets1.root.Workspace.Review, title: "Enter the review flow" },
publish: { target: targets1.root.Workspace.Finished, title: "Publish without review" }
},
transition14: { destination: { history: targets1.root.Workspace.recent } },
Expand Down Expand Up @@ -203,8 +203,8 @@ export const transitionSemanticsMachine = Machine.make({
branches: "transition4",
resolve: ({ containingState, select }) =>
containingState.preferredRoute === "review"
? select.review.decoded(new Review({ requestedBy: "initial route" }))
: select.draft.decoded(new Draft({ text: "", autosaves: 0 }))
? select.review({ data: new Review({ requestedBy: "initial route" }), decoded: true })
: select.draft({ data: new Draft({ text: "", autosaves: 0 }), decoded: true })
}
},
Draft: {
Expand All @@ -219,8 +219,8 @@ export const transitionSemanticsMachine = Machine.make({
branches: "transition7",
resolve: ({ event, select }) =>
event.mode === "publish"
? select.publish.decoded(new WorkspaceFinished({ result: "published directly" }))
: select.review.decoded(new Review({ requestedBy: event.requestedBy }))
? select.publish({ data: new WorkspaceFinished({ result: "published directly" }), decoded: true })
: select.review({ data: new Review({ requestedBy: event.requestedBy }), decoded: true })
},
Refresh: { none: true, reenter: true },
Ignore: { none: true },
Expand Down Expand Up @@ -282,14 +282,14 @@ export const transitionSemanticsMachine = Machine.make({
Paused: {
on: {
Create: {
initial: targets1.root.Workspace,
target: targets1.root.Workspace,
decoded: true,
data: ({ event }) => (new Workspace({ revision: 0, preferredRoute: event.route }))
},
ResumeShallow: { branches: "transition14", resolve: ({ select: { destination: target } }) => target() },
ResumeDeep: { branches: "transition15", resolve: ({ select: { destination: target } }) => target() },
Restart: {
initial: targets1.root.Workspace,
target: targets1.root.Workspace,
decoded: true,
data: () => (new Workspace({ revision: 0, preferredRoute: "draft" }))
}
Expand Down
15 changes: 8 additions & 7 deletions packages/effect-machine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ const machine = Machine.make({
branches: "search",
resolve: ({ event, select }) =>
event.query.length > 0
? select.loading.from({ query: event.query })
: select.idle.from()
? select.loading({ data: { query: event.query } })
: select.idle()
}
}
}
Expand All @@ -221,9 +221,9 @@ const machine = Machine.make({
Each selector is bound to its declared destination. Its constructor rejects a
payload belonging to another branch. A single-target group works the same way;
there is no second path declaration in the resolver. For a compound target,
`select.checkout.from(parentValues, child => child.Review.from(childValues))`
`select.checkout({ data: parentValues, states: { Review: { data: childValues } } })`
constructs the explicitly selected subtree. Parallel constructors require every
entered region. Every compound has an initial edge, including inactive branches. Final outputs,
entered region; source-local construction can retain active sibling regions. Every compound has an initial edge, including inactive branches. Final outputs,
history defaults, and choices remain part of machine readiness checking.

Use `guard: context => boolean` to decline before construction or commands.
Expand All @@ -239,9 +239,10 @@ All references come from the same root descriptor supplied to `make`:
| Declaration | Meaning |
| ----------------------------------------------------- | ------------------------------------------------------------------------- |
| `{ target: targets.root.Checkout.Review, data: ... }` | Enter a declared destination with its value. |
| `{ initial: targets.root.Checkout, data: ... }` | Enter the declared initial configuration of a compound or parallel state. |
| `{ target: targets.root.Checkout, data: ... }` | Enter the declared initial configuration of a compound or parallel state. |
| `{ history: targets.root.Checkout.recent }` | Restore a declared history state. |
| `{ update: targets.root.Checkout, data: ... }` | Replace a retained active owner's value. |
| `{ target: targets.root, input: ... }` | Reconstruct root and its initial children using fresh machine input. |
| `{ update: targets.root, data: ... }` | Replace root data and retain active descendants. |
| `{ none: true }` | Accept an event without changing the configuration. |

Expand All @@ -260,12 +261,12 @@ Save: {

The complete replacement values are validated before either change is applied.
Advanced construction declares both references in a branch and uses
`select.saved.from(destinationValues).update.from(ownerValues)`.
`select.saved({ data: destinationValues, update: { data: ownerValues } })`.
A retained owner must be active for that source and remain active through the
transition. A sibling region's value cannot be updated through this operation.

The runtime transition API does not replace arbitrary complete root
configurations. Startup follows the initial declarations in `.handle`; history
configurations. Root targets accept fresh input and follow the initial declarations in `.handle`; history
defaults retain complete subtree construction for restoration.

### Protocols and ownership
Expand Down
Loading