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
6 changes: 6 additions & 0 deletions .changeset/declared-state-path-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@typeonce/effect-machine-devtools": patch
"@typeonce/oxlint-plugin-effect-machine": patch
---

Show transition declarations with declared state paths such as `{ target: "Checkout.Review" }` and `{ initialize: … }` in the devtools. `no-async-planning-callback` also checks `initialize` input mappers.
20 changes: 20 additions & 0 deletions .changeset/declared-state-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@typeonce/effect-machine": minor
---

Replace `Machine.targets(Root)` references with declared state paths. Transitions, initial edges, history, owner updates, and named branches now take dotted path strings, spelled the same way as snapshot paths. They are checked against the root passed to `make` and suggested by the editor. The root node is `"root"`, so a top-level state can no longer use that name.

Restart the machine from fresh input with `initialize`, which replaces root targets. Machines without input use `initialize: true`, and branch declarations use `{ initialize: true }` with `select.branch({ input })`.

```ts
Machine.make({ root: Root, input: Input, events: Events }).handle({
initial: { target: "Idle" },
on: {
Increment: { update: "root", data: ({ root }) => ({ ...root, count: root.count + 1 }) },
Reset: { initialize: ({ event }) => ({ id: event.id }), reenter: true }
},
states: { Idle: { on: { Resume: { history: "Checkout.recent" } } } }
})
```

To migrate, remove `Machine.targets`. Replace `targets.root.A.B` with `"A.B"`, `update: targets.root` with `update: "root"`, and `{ target: targets.root, input }` with `{ initialize: input }`. Rename any top-level state named `root`. Declarations stored in variables before they reach `make` or `.handle` need `as const`.
2 changes: 1 addition & 1 deletion .changeset/typed-operation-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@

Check the public `Machine` operations against their implementations at compile time.

`Machine.start`, `resume`, `plan`, `planInitial`, `can`, `enabled`, `isFinal`, `encodeSnapshot`, `decodeSnapshot`, `make`, `targets`, and the event protocol builders no longer rely on unchecked casts, so their documented signatures now stay in sync with runtime behavior. Planning failures are classified consistently across runtime strategies: non-stabilization and schema failures stay typed, startup throws become `StartupError`, and other handler throws remain defects.
`Machine.start`, `resume`, `plan`, `planInitial`, `can`, `enabled`, `isFinal`, `encodeSnapshot`, `decodeSnapshot`, `make`, and the event protocol builders no longer rely on unchecked casts, so their documented signatures now stay in sync with runtime behavior. Planning failures are classified consistently across runtime strategies: non-stabilization and schema failures stay typed, startup throws become `StartupError`, and other handler throws remain defects.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ import { Effect } from "effect"
const Root = Machine.state({
states: { Locked: {}, Unlocked: {} }
})
const targets = Machine.targets(Root)
const Events = Machine.events({ Coin: {}, Push: {} })
const Turnstile = Machine.make({
root: Root,
events: Events
}).handle({
initial: {
target: targets.root.Locked
target: "Locked"
},
states: {
Locked: { on: { Coin: { target: targets.root.Unlocked } } },
Unlocked: { on: { Push: { target: targets.root.Locked } } }
Locked: { on: { Coin: { target: "Unlocked" } } },
Unlocked: { on: { Push: { target: "Locked" } } }
}
})
const program = Effect.gen(function*() {
Expand Down
4 changes: 0 additions & 4 deletions api-reference.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,6 @@
"title": "Transition declarations",
"description": "Target selection, resolution, branching, and commands available to state and invocation transitions.",
"roots": [
{
"reflection": "Targets",
"label": "State references"
},
{
"reflection": "Machine.Transition",
"label": "Inline transitions"
Expand Down
17 changes: 8 additions & 9 deletions packages/devtools/src/internal/browser/example-machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,18 @@ export const snapshot = {
const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining<{
readonly "": typeof States.node
}, "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 }
destination: { target: "application.workflow.running", update: "application.workflow" }
}
},
id: "inspection-example",
root: States,
events: Machine.eventsFromSchemas(Start, Finish, Disconnect, Refresh)
}).handle({
initial: {
target: Machine.targets(States).root.application,
target: "application",
decoded: true,
data: new Application({ workspace: "effect-machine", revision: 7 })
},
Expand All @@ -126,7 +125,7 @@ export const machine = Machine.make({
states: {
workflow: {
initial: {
target: Machine.targets(States).root.application.workflow.idle,
target: "application.workflow.idle",
decoded: true,
data: new Idle({})
},
Expand All @@ -149,23 +148,23 @@ export const machine = Machine.make({
})
},
Refresh: {
update: targets1.root.application.workflow,
update: "application.workflow",
decoded: true,
data: () => (new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
}
}
},
running: {
initial: {
target: Machine.targets(States).root.application.workflow.running.editing,
target: "application.workflow.running.editing",
decoded: true,
data: ({}) => new Editing({})
},
states: {
editing: {
on: {
Finish: {
target: targets1.root.application.workflow.running.complete,
target: "application.workflow.running.complete",
decoded: true,
data: () => (new Complete({}))
}
Expand All @@ -178,15 +177,15 @@ export const machine = Machine.make({
},
connection: {
initial: {
target: Machine.targets(States).root.application.connection.online,
target: "application.connection.online",
decoded: true,
data: new Online({})
},
states: {
online: {
on: {
Disconnect: {
target: targets1.root.application.connection.offline,
target: "application.connection.offline",
decoded: true,
data: () => (new Offline({}))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ const ReviewEvents = Machine.eventsFromSchemas(Schema.TaggedUnion({
}))
const saveReview: Effect.Effect<string, string> = Effect.succeed("deterministic-chart")
const publishReview = Effect.succeed("deterministic-chart")
const targets1 = Machine.targets(ReviewStates)
export const hierarchyRoutingMachine = Machine.make({
branches: {
transition1: {
save: { target: targets1.root.Workflow.Saving, title: "Save the review" },
invalid: { target: targets1.root.Workflow.Review.Failed, title: "Show validation failure" }
save: { target: "Workflow.Saving", title: "Save the review" },
invalid: { target: "Workflow.Review.Failed", title: "Show validation failure" }
}
},
effects: { source1: Effect.suspend(() => saveReview), source2: Effect.suspend(() => publishReview) },
Expand All @@ -42,18 +41,18 @@ export const hierarchyRoutingMachine = Machine.make({
events: ReviewEvents
}).handle({
initial: {
target: Machine.targets(ReviewStates).root.Workflow
target: "Workflow"
},
states: {
Workflow: {
initial: {
target: Machine.targets(ReviewStates).root.Workflow.Review,
target: "Workflow.Review",
data: { title: "A deterministic chart" }
},
states: {
Review: {
initial: {
target: Machine.targets(ReviewStates).root.Workflow.Review.Form
target: "Workflow.Review.Form"
},
on: {
Submit: {
Expand All @@ -73,9 +72,9 @@ export const hierarchyRoutingMachine = Machine.make({
invoke: {
src: "source1",
id: "save-review",
onDone: { target: targets1.root.Workflow.Publishing },
onDone: { target: "Workflow.Publishing" },
onFailure: {
target: targets1.root.Workflow.Review.Failed,
target: "Workflow.Review.Failed",
data: () => ({ message: "The review could not be saved." })
}
}
Expand All @@ -84,7 +83,7 @@ export const hierarchyRoutingMachine = Machine.make({
invoke: {
src: "source2",
id: "publish-review",
onDone: { target: targets1.root.Workflow.Complete, data: ({ output }) => ({ slug: output }) }
onDone: { target: "Workflow.Complete", data: ({ output }) => ({ slug: output }) }
}
},
Complete: {}
Expand Down
44 changes: 21 additions & 23 deletions packages/devtools/src/internal/browser/invoke-outcomes-example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,21 @@ const ChildStates = Machine.state({
Done: { schema: ChildDone, type: "final", output: Schema.String }
}
})
const targets1 = Machine.targets(ChildStates)
export const invokeGalleryChildMachine = Machine.make({
id: "invoke-gallery-child",
root: ChildStates,
events: Machine.eventsFromSchemas(FinishChild)
}).handle({
initial: {
target: Machine.targets(ChildStates).root.Working,
target: "Working",
decoded: true,
data: new ChildWorking({ task: "render-preview" })
},
states: {
Working: {
on: {
FinishChild: {
target: targets1.root.Done,
target: "Done",
decoded: true,
data: ({ state }) => (new ChildDone({ result: `${state.task}:complete` }))
}
Expand Down Expand Up @@ -122,11 +121,10 @@ const GalleryStates = Machine.state({
Failed
}
})
const targets2 = Machine.targets(GalleryStates)
export const invokeOutcomesMachine = Machine.make({
branches: {
transition13: {
ready: { target: targets2.root.Completed, title: "Worker reports ready" },
ready: { target: "Completed", title: "Worker reports ready" },
waiting: { none: true, title: "Worker is still starting" }
}
},
Expand All @@ -149,44 +147,44 @@ export const invokeOutcomesMachine = Machine.make({
internalEvents: GalleryInternalEvents
}).handle({
initial: {
target: Machine.targets(GalleryStates).root.Gallery,
target: "Gallery",
decoded: true,
data: new Gallery({ selectedDemo: null })
},
states: {
Gallery: {
initial: {
target: Machine.targets(GalleryStates).root.Gallery.Choose,
target: "Gallery.Choose",
decoded: true,
data: ({}) => new Choose({})
},
on: {
Reset: { target: targets2.root.Gallery.Choose, decoded: true, data: () => (new Choose({})) }
Reset: { target: "Gallery.Choose", decoded: true, data: () => (new Choose({})) }
},
states: {
Choose: {
on: {
RunEffect: {
target: targets2.root.Gallery.LoadingDocument,
target: "Gallery.LoadingDocument",
decoded: true,
data: ({ event }) => (new LoadingDocument({ request: event.request }))
},
RunStream: {
target: targets2.root.Gallery.StreamingUpdates,
target: "Gallery.StreamingUpdates",
decoded: true,
data: () => (new StreamingUpdates({ values: [] }))
},
RunTimer: {
target: targets2.root.Gallery.WaitingForTimeout,
target: "Gallery.WaitingForTimeout",
decoded: true,
data: () => (new WaitingForTimeout({ delay: "2 seconds" }))
},
RunProcess: {
target: targets2.root.Gallery.WatchingProcess,
target: "Gallery.WatchingProcess",
decoded: true,
data: () => (new WatchingProcess({ revision: 1 }))
},
RunChild: { target: targets2.root.Gallery.RunningChild, decoded: true, data: () => (new RunningChild({})) }
RunChild: { target: "Gallery.RunningChild", decoded: true, data: () => (new RunningChild({})) }
}
},
LoadingDocument: {
Expand All @@ -195,12 +193,12 @@ export const invokeOutcomesMachine = Machine.make({
id: "load-document",
input: (context) => context,
onDone: {
target: targets2.root.Completed,
target: "Completed",
decoded: true,
data: ({ output }) => (new Completed({ source: "effect", result: output }))
},
onFailure: {
target: targets2.root.Failed,
target: "Failed",
decoded: true,
data: ({ error }) => (new Failed({ source: "effect", message: error }))
}
Expand All @@ -217,19 +215,19 @@ export const invokeOutcomesMachine = Machine.make({
}
},
onDone: {
target: targets2.root.Completed,
target: "Completed",
decoded: true,
data: ({ state }) => (new Completed({ source: "stream", result: state.values.join(", ") }))
},
onFailure: {
target: targets2.root.Failed,
target: "Failed",
decoded: true,
data: ({ error }) => (new Failed({ source: "stream", message: error }))
}
},
on: {
StreamValue: {
target: targets2.root.Gallery.StreamingUpdates,
target: "Gallery.StreamingUpdates",
decoded: true,
data: ({ event, state }) => (new StreamingUpdates({ values: [...state.values, event.value] }))
}
Expand All @@ -240,7 +238,7 @@ export const invokeOutcomesMachine = Machine.make({
src: "source3",
id: "request-timeout",
onDone: {
target: targets2.root.Completed,
target: "Completed",
decoded: true,
data: () => (new Completed({ source: "timer", result: "timeout elapsed" }))
}
Expand All @@ -252,7 +250,7 @@ export const invokeOutcomesMachine = Machine.make({
id: "status-worker",
address: Machine.childAddress("status-worker"),
onFailure: {
target: targets2.root.Failed,
target: "Failed",
decoded: true,
data: ({ error }) => (new Failed({ source: "process", message: String(error) }))
},
Expand All @@ -269,7 +267,7 @@ export const invokeOutcomesMachine = Machine.make({
invoke: {
src: "source5",
onDone: {
target: targets2.root.Completed,
target: "Completed",
decoded: true,
data: ({ output }) => (new Completed({ source: "child machine", result: output }))
}
Expand All @@ -279,12 +277,12 @@ export const invokeOutcomesMachine = Machine.make({
},
Completed: {
on: {
Reset: { target: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
Reset: { target: "Gallery", decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
}
},
Failed: {
on: {
Reset: { target: targets2.root.Gallery, decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
Reset: { target: "Gallery", decoded: true, data: () => (new Gallery({ selectedDemo: null })) }
}
}
}
Expand Down
Loading
Loading