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
19 changes: 19 additions & 0 deletions .changeset/handler-owned-initial-edges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@typeonce/effect-machine": minor
"@typeonce/effect-machine-react": minor
"@typeonce/effect-machine-devtools": minor
"@typeonce/oxlint-plugin-effect-machine": minor
---

Declare initial edges and data together in `.handle`. Remove `initial` from `Machine.state`, remove `initial` and `initialConfiguration` from `Machine.make`, and move shared startup data into the root handler's `root` value or input callback. Each compound declares `initial: { target, data? }`; parallel handlers use an `initial` map of region data. Initial targets must be direct children and remain inspectable without executing constructors.

```ts
const root = Machine.state({ states: { Locked: {}, Unlocked: {} } })
const targets = Machine.targets(root)
const machine = Machine.make({ root, events: Machine.events({ Coin: {} }) }).handle({
initial: { target: targets.root.Locked },
states: { Locked: { on: { Coin: { target: targets.root.Unlocked } } } }
})
```

Replace declarative `from` callbacks with `data`, which accepts literals or callbacks. Supply already-decoded values with `{ decoded: true, data: valueOrCallback }`. For root or region data that itself contains `decoded: true` and `data` fields, use a callback returning the ordinary schema input. Bound branch and history builders retain `.from` and `.decoded`. Replace command-producing `initialize` callbacks with `entry`/`exit` handlers or invocations; definitions become executable only after `.handle` captures their initial declarations.
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,23 @@ Effect-native, schema-first, completely type-safe state machines and statecharts
```ts
import { Machine } from "@typeonce/effect-machine"
import { Effect } from "effect"

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({
const Turnstile = Machine.make({
root: Root,
events: Events
}).handle({
initial: {
target: targets.root.Locked
},
states: {
Locked: { on: { Coin: { target: targets.root.Unlocked } } },
Unlocked: { on: { Push: { target: targets.root.Locked } } }
}
})

const program = Effect.gen(function*() {
const ref = yield* Machine.start(Turnstile)
yield* ref.send(Events.Coin())
Expand Down
6 changes: 1 addition & 5 deletions api-reference.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,12 @@
{
"owner": "make",
"title": "Machine definition configuration",
"description": "Parameters accepted by `Machine.make` and its initial-state declaration.",
"description": "Protocols, root topology, registered sources, and named branches accepted by Machine.make.",
"roots": [
{
"declaration": "make",
"parameter": "config",
"label": "Configuration"
},
{
"reflection": "Machine.InitialTransitionTarget",
"label": "Initial target resolution"
}
]
},
Expand Down
121 changes: 79 additions & 42 deletions packages/devtools/src/internal/browser/example-machine.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,52 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

// Shared by the live browser UI and its project-inspection fixture.

class Application extends Schema.TaggedClass<Application>("Application")("Application", {
workspace: Schema.String,
revision: Schema.Number
}) {}
}) {
}
class Workflow extends Schema.TaggedClass<Workflow>("Workflow")("Workflow", {
document: Schema.String,
unsavedChanges: Schema.Number
}) {}
class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {}
class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {}
class Editing extends Schema.TaggedClass<Editing>("Editing")("Editing", {}) {}
class Complete extends Schema.TaggedClass<Complete>("Complete")("Complete", {}) {}
class Connection extends Schema.TaggedClass<Connection>("Connection")("Connection", {}) {}
class Online extends Schema.TaggedClass<Online>("Online")("Online", {}) {}
class Offline extends Schema.TaggedClass<Offline>("Offline")("Offline", {}) {}
class Disabled extends Schema.TaggedClass<Disabled>("Disabled")("Disabled", {}) {}
class Start extends Schema.TaggedClass<Start>("Start")("Start", {}) {}
class Finish extends Schema.TaggedClass<Finish>("Finish")("Finish", {}) {}
class Disconnect extends Schema.TaggedClass<Disconnect>("Disconnect")("Disconnect", {}) {}
class Refresh extends Schema.TaggedClass<Refresh>("Refresh")("Refresh", {}) {}

}) {
}
class Idle extends Schema.TaggedClass<Idle>("Idle")("Idle", {}) {
}
class Running extends Schema.TaggedClass<Running>("Running")("Running", {}) {
}
class Editing extends Schema.TaggedClass<Editing>("Editing")("Editing", {}) {
}
class Complete extends Schema.TaggedClass<Complete>("Complete")("Complete", {}) {
}
class Connection extends Schema.TaggedClass<Connection>("Connection")("Connection", {}) {
}
class Online extends Schema.TaggedClass<Online>("Online")("Online", {}) {
}
class Offline extends Schema.TaggedClass<Offline>("Offline")("Offline", {}) {
}
class Disabled extends Schema.TaggedClass<Disabled>("Disabled")("Disabled", {}) {
}
class Start extends Schema.TaggedClass<Start>("Start")("Start", {}) {
}
class Finish extends Schema.TaggedClass<Finish>("Finish")("Finish", {}) {
}
class Disconnect extends Schema.TaggedClass<Disconnect>("Disconnect")("Disconnect", {}) {
}
class Refresh extends Schema.TaggedClass<Refresh>("Refresh")("Refresh", {}) {
}
const States = Machine.state({
initial: "application",
states: {
application: {
schema: Application,
type: "parallel",
states: {
workflow: {
schema: Workflow,
initial: "idle",
states: {
idle: Idle,
running: {
schema: Running,
initial: "editing",
states: {
editing: Editing,
complete: {
Expand All @@ -54,7 +62,6 @@ const States = Machine.state({
},
connection: {
schema: Connection,
initial: "online",
states: {
online: Online,
offline: Offline
Expand All @@ -65,7 +72,6 @@ const States = Machine.state({
disabled: Disabled
}
})

export const snapshot = {
path: "" as const,
value: undefined,
Expand All @@ -86,29 +92,44 @@ export const snapshot = {
}
}
}

const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining<
{ readonly "": typeof States.node },
"application.workflow"
> => 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 }
}
},

id: "inspection-example",
root: States,
events: Machine.eventsFromSchemas(Start, Finish, Disconnect, Refresh),
initialConfiguration: (to) => to.resolve(() => snapshot)
events: Machine.eventsFromSchemas(Start, Finish, Disconnect, Refresh)
}).handle({
initial: {
target: Machine.targets(States).root.application,
decoded: true,
data: new Application({ workspace: "effect-machine", revision: 7 })
},
states: {
application: {
initial: {
workflow: {
decoded: true,
data: new Workflow({ document: "Machine.ts", unsavedChanges: 2 })
},
connection: {
decoded: true,
data: new Connection({})
}
},
states: {
workflow: {
initial: {
target: Machine.targets(States).root.application.workflow.idle,
decoded: true,
data: new Idle({})
},
history: {
recent: {
default: initialWorkflow
Expand All @@ -120,42 +141,58 @@ 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.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 }))
decoded: true,
data: () => (new Workflow({ document: "Machine.ts", unsavedChanges: 0 }))
}
}
},
running: {
initialize: ({ builder }) => builder.decoded(new Editing({})),
initial: {
target: Machine.targets(States).root.application.workflow.running.editing,
decoded: true,
data: ({}) => new Editing({})
},
states: {
editing: {
on: {
Finish: {
target: targets1.root.application.workflow.running.complete,
decoded: () => (new Complete({}))
decoded: true,
data: () => (new Complete({}))
}
}
}
},
complete: {}
}
}
}
},
connection: {
initial: {
target: Machine.targets(States).root.application.connection.online,
decoded: true,
data: new Online({})
},
states: {
online: {
on: {
Disconnect: { target: targets1.root.application.connection.offline, decoded: () => (new Offline({})) }
Disconnect: {
target: targets1.root.application.connection.offline,
decoded: true,
data: () => (new Offline({}))
}
}
}
},
offline: {}
}
}
}
}
},
disabled: {}
}
})
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { Machine } from "@typeonce/effect-machine"
import { Effect, Schema } from "effect"

const ReviewState = Schema.TaggedUnion({
Review: { title: Schema.String },
ReviewFailed: { message: Schema.String },
Complete: { slug: Schema.String }
})

const ReviewStates = Machine.state({
initial: "Workflow",
states: {
Workflow: {
initial: "Review",
states: {
Review: {
schema: ReviewState.cases.Review,
initial: "Form",
states: {
Form: {},
Failed: ReviewState.cases.ReviewFailed
Expand All @@ -28,16 +23,11 @@ const ReviewStates = Machine.state({
}
}
})

const ReviewEvents = Machine.eventsFromSchemas(
Schema.TaggedUnion({
Submit: { route: Schema.Literals(["save", "invalid"]) }
})
)

const ReviewEvents = Machine.eventsFromSchemas(Schema.TaggedUnion({
Submit: { route: Schema.Literals(["save", "invalid"]) }
}))
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: {
Expand All @@ -47,23 +37,24 @@ export const hierarchyRoutingMachine = Machine.make({
}
},
effects: { source1: Effect.suspend(() => saveReview), source2: Effect.suspend(() => publishReview) },

id: "hierarchy-routing",
root: ReviewStates,
events: ReviewEvents,
initialConfiguration: (root) =>
root.resolve(({ target }) =>
target.from((to) =>
to.Workflow.from((workflow) =>
workflow.Review.from({ title: "A deterministic chart" }, (review) => review.Form.from())
)
)
)
events: ReviewEvents
}).handle({
initial: {
target: Machine.targets(ReviewStates).root.Workflow
},
states: {
Workflow: {
initial: {
target: Machine.targets(ReviewStates).root.Workflow.Review,
data: { title: "A deterministic chart" }
},
states: {
Review: {
initial: {
target: Machine.targets(ReviewStates).root.Workflow.Review.Form
},
on: {
Submit: {
branches: "transition1",
Expand All @@ -85,15 +76,15 @@ export const hierarchyRoutingMachine = Machine.make({
onDone: { target: targets1.root.Workflow.Publishing },
onFailure: {
target: targets1.root.Workflow.Review.Failed,
from: () => ({ message: "The review could not be saved." })
data: () => ({ message: "The review could not be saved." })
}
}
},
Publishing: {
invoke: {
src: "source2",
id: "publish-review",
onDone: { target: targets1.root.Workflow.Complete, from: ({ output }) => ({ slug: output }) }
onDone: { target: targets1.root.Workflow.Complete, data: ({ output }) => ({ slug: output }) }
}
},
Complete: {}
Expand Down
Loading