From 1d1c16cecb168db15b1b0eba2a6695cc774a0a27 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Wed, 9 Sep 2026 13:32:51 +0200 Subject: [PATCH 1/3] Define machines with typed targets and registered programs --- .changeset/declarative-machine-definition.md | 12 + README.md | 5 +- api-reference.config.json | 208 +- .../src/internal/browser/example-machine.ts | 39 +- .../browser/hierarchy-routing-example.ts | 51 +- .../browser/invoke-outcomes-example.ts | 190 +- .../browser/layout-resilience-example.ts | 118 +- .../browser/parallel-completion-example.ts | 109 +- .../browser/parallel-owner-routing-example.ts | 43 +- .../src/internal/browser/planner-example.ts | 33 +- .../browser/protocol-events-example.ts | 130 +- .../shared-terminal-routing-example.ts | 149 +- .../browser/transition-semantics-example.ts | 139 +- .../src/internal/browser/visualizer-app.ts | 26 +- .../src/internal/browser/visualizer-model.ts | 105 +- .../InteractiveTextVisualization.test.ts | 38 +- .../test/internal/browser/StaticChart.test.ts | 5 +- .../test/MachineAtom.test.tsx | 9 +- .../test/RootContext.test.tsx | 8 +- .../typetest/MachineAtom.tst.ts | 3 +- packages/effect-machine/README.md | 851 +--- packages/effect-machine/docs/agent-guide.md | 157 +- .../effect-machine/docs/machine-review.md | 68 +- packages/effect-machine/docs/root-api.md | 124 +- .../runtime/effect-machine-compatibility.mjs | 56 +- .../perf/types/adapter-readiness-control.ts | 3 +- .../effect-machine/perf/types/composition.ts | 43 +- .../perf/types/definition-variants.ts | 15 +- .../perf/types/dynamic-invoke.ts | 33 +- .../perf/types/exact-channels.ts | 27 +- packages/effect-machine/perf/types/handle.ts | 11 +- .../perf/types/named-branches.ts | 46 +- .../types/transition-construction-control.ts | 11 +- .../perf/types/transition-construction.ts | 48 +- packages/effect-machine/src/Machine.ts | 3788 +++++++---------- .../src/internal/machine/declaration.ts | 140 + .../src/internal/machine/invocation.ts | 4 +- .../internal/machine/invocationDefinition.ts | 2 +- .../src/internal/machine/machine.ts | 583 +-- .../src/internal/machine/planner.ts | 66 +- .../src/internal/machine/targetReference.ts | 40 + .../internal/testing/machine/finiteModel.ts | 85 +- packages/effect-machine/test/examples/can.ts | 2 +- .../effect-machine/test/examples/guard.ts | 13 +- .../machine/ProtocolValidation.test.ts | 11 +- .../test/internal/machine/activities.test.ts | 54 +- .../internal/machine/rootStrategies.test.ts | 34 +- .../machine/strategyDifferential.test.ts | 394 +- .../transitionConstructionStrategies.test.ts | 89 +- .../machine/ActivityLifecycleModel.test.ts | 249 +- .../test/machine/Choice.test.ts | 156 +- .../test/machine/Declarative.test.ts | 208 + .../test/machine/DeepHandlers.test.ts | 12 +- .../test/machine/DynamicChildren.test.ts | 185 +- .../test/machine/History.test.ts | 84 +- .../test/machine/InitialEntry.test.ts | 46 +- .../test/machine/Inspection.test.ts | 3 +- .../test/machine/Invoke.test.ts | 164 +- .../test/machine/LiveInspection.test.ts | 36 +- .../test/machine/LocalTargetWith.test.ts | 53 +- .../test/machine/Machine.test.ts | 2006 +++++---- .../test/machine/MachineReferences.test.ts | 78 +- .../test/machine/ProtocolOwnership.test.ts | 16 +- .../test/machine/Resume.test.ts | 133 +- .../effect-machine/test/machine/Root.test.ts | 53 +- .../test/machine/RuntimeDifferential.test.ts | 101 +- .../test/machine/Scheduling.test.ts | 22 +- .../machine/SnapshotCodecAdversarial.test.ts | 6 +- .../test/machine/SnapshotContext.test.ts | 71 +- .../test/machine/StateUpdate.test.ts | 179 +- .../test/machine/StructuralStates.test.ts | 60 +- .../test/machine/Targets.test.ts | 31 + .../test/machine/Totality.test.ts | 3 +- .../machine/TransitionConstruction.test.ts | 107 +- .../test/machine/Visualization.test.ts | 108 +- .../test/testing/Coverage.test.ts | 59 +- .../test/testing/Exploration.test.ts | 23 +- .../test/testing/Invariant.test.ts | 22 +- .../test/testing/MachineTest.test.ts | 37 +- .../effect-machine/test/testing/Probe.test.ts | 54 +- .../test/testing/ReferenceModel.test.ts | 22 +- .../test/testing/Runtime.test.ts | 75 +- .../test/testing/TraceValues.test.ts | 10 +- .../test/testing/Verification.test.ts | 120 +- .../unstable/cluster/ClusterMachine.test.ts | 57 +- .../unstable/reactivity/AtomMachine.test.ts | 188 +- .../typetest/machine/Activities.tst.ts | 7 +- .../typetest/machine/Choice.tst.ts | 50 +- .../typetest/machine/Declarative.tst.ts | 222 + .../typetest/machine/DynamicChildren.tst.ts | 104 +- .../typetest/machine/EventByTag.tst.ts | 17 +- .../typetest/machine/EventConstructors.tst.ts | 33 +- .../typetest/machine/History.tst.ts | 26 +- .../typetest/machine/InitialEntry.tst.ts | 27 +- .../typetest/machine/Inspection.tst.ts | 9 +- .../typetest/machine/Machine.tst.ts | 873 ++-- .../typetest/machine/MachineReferences.tst.ts | 96 +- .../typetest/machine/Readiness.tst.ts | 32 +- .../typetest/machine/Resume.tst.ts | 4 +- .../typetest/machine/SnapshotContext.tst.ts | 43 +- .../typetest/machine/StateUpdate.tst.ts | 345 +- .../typetest/machine/StructuralStates.tst.ts | 35 +- .../machine/TransitionConstruction.tst.ts | 188 +- .../typetest/testing/Coverage.tst.ts | 3 +- .../typetest/testing/Exploration.tst.ts | 5 +- .../typetest/testing/Invariant.tst.ts | 5 +- .../typetest/testing/MachineTest.tst.ts | 20 +- .../typetest/testing/Probe.tst.ts | 5 +- .../unstable/cluster/ClusterMachine.tst.ts | 11 +- .../unstable/reactivity/AtomMachine.tst.ts | 20 +- packages/oxlint-plugin/README.md | 188 +- .../oxlint-plugin/src/internal/planning.ts | 24 + .../internal/rules/noAsyncPlanningCallback.ts | 2 +- .../rules/noConflictingInvocationIdentity.ts | 91 +- .../src/internal/rules/noRedundantResolve.ts | 38 +- .../test/noAsyncPlanningCallback.test.ts | 12 + .../test/noBrowserApiInPlanning.test.ts | 2 + .../noConflictingInvocationIdentity.test.ts | 17 + .../test/noNondeterministicPlanning.test.ts | 2 + .../test/noRedundantResolve.test.ts | 7 + scripts/api-reference/api-reference.test.mjs | 49 + scripts/api-reference/normalize.mjs | 1 + scripts/fixtures/consumer/consumer.ts | 31 +- scripts/fixtures/consumer/deep-bound.ts | 48 +- scripts/invoke-autocomplete.test.mjs | 197 +- scripts/type-performance.mjs | 4 +- 126 files changed, 8466 insertions(+), 7482 deletions(-) create mode 100644 .changeset/declarative-machine-definition.md create mode 100644 packages/effect-machine/src/internal/machine/declaration.ts create mode 100644 packages/effect-machine/src/internal/machine/targetReference.ts create mode 100644 packages/effect-machine/test/machine/Declarative.test.ts create mode 100644 packages/effect-machine/test/machine/Targets.test.ts create mode 100644 packages/effect-machine/typetest/machine/Declarative.tst.ts diff --git a/.changeset/declarative-machine-definition.md b/.changeset/declarative-machine-definition.md new file mode 100644 index 00000000..7abfe454 --- /dev/null +++ b/.changeset/declarative-machine-definition.md @@ -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. diff --git a/README.md b/README.md index 3278c8fc..da6db4ba 100644 --- a/README.md +++ b/README.md @@ -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 } } } } }) diff --git a/api-reference.config.json b/api-reference.config.json index 671b6fa9..8bd3212f 100644 --- a/api-reference.config.json +++ b/api-reference.config.json @@ -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" + } ] }, { @@ -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" + } ] } ], @@ -87,12 +107,30 @@ "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" + } ] }, { @@ -100,8 +138,15 @@ "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" + } ] }, { @@ -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" + } ] }, { @@ -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" + } ] }, { @@ -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" + } ] }, { @@ -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" + } ] } ], @@ -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" + ] } ] } diff --git a/packages/devtools/src/internal/browser/example-machine.ts b/packages/devtools/src/internal/browser/example-machine.ts index c73ae275..12253aaf 100644 --- a/packages/devtools/src/internal/browser/example-machine.ts +++ b/packages/devtools/src/internal/browser/example-machine.ts @@ -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), @@ -110,19 +117,18 @@ 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: { @@ -130,7 +136,10 @@ export const machine = Machine.make({ 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({})) + } } } } @@ -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({})) } } } } diff --git a/packages/devtools/src/internal/browser/hierarchy-routing-example.ts b/packages/devtools/src/internal/browser/hierarchy-routing-example.ts index 936ed329..f2506947 100644 --- a/packages/devtools/src/internal/browser/hierarchy-routing-example.ts +++ b/packages/devtools/src/internal/browser/hierarchy-routing-example.ts @@ -38,7 +38,16 @@ const ReviewEvents = Machine.eventsFromSchemas( const saveReview: Effect.Effect = 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" } + } + }, + effects: { source1: Effect.suspend(() => saveReview), source2: Effect.suspend(() => publishReview) }, + id: "hierarchy-routing", root: ReviewStates, events: ReviewEvents, @@ -56,21 +65,13 @@ export const hierarchyRoutingMachine = Machine.make({ states: { Review: { on: { - Submit: (to) => - to.branches({ - save: { - title: "Save the review", - target: to.branch.Workflow.Saving() - }, - invalid: { - title: "Show validation failure", - target: to.local.Failed() - } - }).resolve(({ event, select }) => + Submit: { + branches: "transition1", + resolve: ({ event, select }) => event.route === "save" ? select.save.from() : select.invalid.from({ message: "Add a title before continuing." }) - ) + } }, states: { Form: {}, @@ -78,20 +79,22 @@ export const hierarchyRoutingMachine = Machine.make({ } }, Saving: { - invoke: (from) => - from.effect("save-review", () => saveReview).onDone((to) => to.branch.Workflow.Publishing()).onFailure(( - to - ) => - to.branch.Workflow.Review.Failed().resolve(({ target }) => - target.from({ message: "The review could not be saved." }) - ) - ) + invoke: { + src: "source1", + id: "save-review", + onDone: { target: targets1.root.Workflow.Publishing }, + onFailure: { + target: targets1.root.Workflow.Review.Failed, + from: () => ({ message: "The review could not be saved." }) + } + } }, Publishing: { - invoke: (from) => - from.effect("publish-review", () => publishReview).onDone((to) => - to.branch.Workflow.Complete().resolve(({ output, target }) => target.from({ slug: output })) - ) + invoke: { + src: "source2", + id: "publish-review", + onDone: { target: targets1.root.Workflow.Complete, from: ({ output }) => ({ slug: output }) } + } }, Complete: {} } diff --git a/packages/devtools/src/internal/browser/invoke-outcomes-example.ts b/packages/devtools/src/internal/browser/invoke-outcomes-example.ts index 9a150fbc..0861d3ed 100644 --- a/packages/devtools/src/internal/browser/invoke-outcomes-example.ts +++ b/packages/devtools/src/internal/browser/invoke-outcomes-example.ts @@ -17,6 +17,7 @@ const ChildStates = Machine.state({ } }) +const targets1 = Machine.targets(ChildStates) export const invokeGalleryChildMachine = Machine.make({ id: "invoke-gallery-child", root: ChildStates, @@ -27,10 +28,10 @@ export const invokeGalleryChildMachine = Machine.make({ states: { Working: { on: { - FinishChild: (to) => - to.branch.Done().resolve(({ state, target }) => - target.decoded(new ChildDone({ result: `${state.task}:complete` })) - ) + FinishChild: { + target: targets1.root.Done, + decoded: ({ state }) => (new ChildDone({ result: `${state.task}:complete` })) + } } }, Done: { @@ -110,7 +111,27 @@ const GalleryStates = Machine.state({ } }) +const targets2 = Machine.targets(GalleryStates) export const invokeOutcomesMachine = Machine.make({ + branches: { + transition13: { + ready: { target: targets2.root.Completed, title: "Worker reports ready" }, + waiting: { none: true, title: "Worker is still starting" } + } + }, + effects: { + source1: ({ state }: { readonly state: LoadingDocument }) => + state.request === "fail" ? Effect.fail("document unavailable") : Effect.succeed("document loaded") + }, + streams: { + source2: Stream.suspend(() => + Stream.fromIterable([1, 2, 3]).pipe(Stream.concat(Stream.fail("stream disconnected"))) + ) + }, + timers: { source3: "2 seconds" }, + logic: { source4: processLogic }, + children: { source5: InvokedChild }, + id: "invoke-outcomes", root: GalleryStates, events: GalleryEvents, @@ -126,115 +147,118 @@ export const invokeOutcomesMachine = Machine.make({ Gallery: { initialize: ({ builder }) => builder.decoded(new Choose({})), on: { - Reset: (to) => to.local.Choose().resolve(({ target }) => target.decoded(new Choose({}))) + Reset: { target: targets2.root.Gallery.Choose, decoded: () => (new Choose({})) } }, states: { Choose: { on: { - RunEffect: (to) => - to.local.LoadingDocument().resolve(({ event, target }) => - target.decoded(new LoadingDocument({ request: event.request })) - ), - RunStream: (to) => - to.local.StreamingUpdates().resolve(({ target }) => target.decoded(new StreamingUpdates({ values: [] }))), - RunTimer: (to) => - to.local.WaitingForTimeout().resolve(({ target }) => - target.decoded(new WaitingForTimeout({ delay: "2 seconds" })) - ), - RunProcess: (to) => - to.local.WatchingProcess().resolve(({ target }) => target.decoded(new WatchingProcess({ revision: 1 }))), - RunChild: (to) => to.local.RunningChild().resolve(({ target }) => target.decoded(new RunningChild({}))) + RunEffect: { + target: targets2.root.Gallery.LoadingDocument, + decoded: ({ event }) => (new LoadingDocument({ request: event.request })) + }, + RunStream: { + target: targets2.root.Gallery.StreamingUpdates, + decoded: () => (new StreamingUpdates({ values: [] })) + }, + RunTimer: { + target: targets2.root.Gallery.WaitingForTimeout, + decoded: () => (new WaitingForTimeout({ delay: "2 seconds" })) + }, + RunProcess: { + target: targets2.root.Gallery.WatchingProcess, + decoded: () => (new WatchingProcess({ revision: 1 })) + }, + RunChild: { target: targets2.root.Gallery.RunningChild, decoded: () => (new RunningChild({})) } } }, LoadingDocument: { - invoke: (from) => - from.effect( - "load-document", - ({ state }) => - state.request === "fail" ? Effect.fail("document unavailable") : Effect.succeed("document loaded") - ).onDone((to) => - to.branch.Completed().resolve(({ output, target }) => - target.decoded(new Completed({ source: "effect", result: output })) - ) - ).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ source: "effect", message: error })) - ) - ) + invoke: { + src: "source1", + id: "load-document", + input: (context) => context, + onDone: { + target: targets2.root.Completed, + decoded: ({ output }) => (new Completed({ source: "effect", result: output })) + }, + onFailure: { + target: targets2.root.Failed, + decoded: ({ error }) => (new Failed({ source: "effect", message: error })) + } + } }, StreamingUpdates: { - invoke: (from) => - from.stream( - "document-updates", - () => Stream.fromIterable([1, 2, 3]).pipe(Stream.concat(Stream.fail("stream disconnected"))) - ).onElement((to) => - to.none.resolve(({ element }, enqueue) => { + invoke: { + src: "source2", + id: "document-updates", + onElement: { + none: true, + resolve: ({ element }, enqueue) => { enqueue.raise(GalleryInternalEvents.StreamValue({ value: element })) - }) - ).onDone((to) => - to.branch.Completed().resolve(({ state, target }) => - target.decoded(new Completed({ source: "stream", result: state.values.join(", ") })) - ) - ).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ source: "stream", message: error })) - ) - ), + } + }, + onDone: { + target: targets2.root.Completed, + decoded: ({ state }) => (new Completed({ source: "stream", result: state.values.join(", ") })) + }, + onFailure: { + target: targets2.root.Failed, + decoded: ({ error }) => (new Failed({ source: "stream", message: error })) + } + }, on: { - StreamValue: (to) => - to.local.StreamingUpdates().resolve(({ event, state, target }) => - target.decoded(new StreamingUpdates({ values: [...state.values, event.value] })) - ) + StreamValue: { + target: targets2.root.Gallery.StreamingUpdates, + decoded: ({ event, state }) => (new StreamingUpdates({ values: [...state.values, event.value] })) + } } }, WaitingForTimeout: { - invoke: (from) => - from.timer("request-timeout", "2 seconds").onDone((to) => - to.branch.Completed().resolve(({ target }) => - target.decoded(new Completed({ source: "timer", result: "timeout elapsed" })) - ) - ) + invoke: { + src: "source3", + id: "request-timeout", + onDone: { + target: targets2.root.Completed, + decoded: () => (new Completed({ source: "timer", result: "timeout elapsed" })) + } + } }, WatchingProcess: { - invoke: (from) => - from.logic("status-worker", { - address: Machine.childAddress("status-worker"), - logic: processLogic - }).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ source: "process", message: String(error) })) - ) - ).onSnapshot((to) => - to.branches({ - ready: { title: "Worker reports ready", target: to.branch.Completed() }, - waiting: { title: "Worker is still starting", target: to.none } - }).resolve(({ select, snapshot }) => + invoke: { + src: "source4", + id: "status-worker", + address: Machine.childAddress("status-worker"), + onFailure: { + target: targets2.root.Failed, + decoded: ({ error }) => (new Failed({ source: "process", message: String(error) })) + }, + onSnapshot: { + branches: "transition13", + resolve: ({ select, snapshot }) => snapshot.state === "ready" ? select.ready.decoded(new Completed({ source: "process", result: "ready" })) : select.waiting() - ) - ) + } + } }, RunningChild: { - invoke: (from) => - from.child(InvokedChild).onDone((to) => - to.branch.Completed().resolve(({ output, target }) => - target.decoded(new Completed({ source: "child machine", result: output })) - ) - ) + invoke: { + src: "source5", + onDone: { + target: targets2.root.Completed, + decoded: ({ output }) => (new Completed({ source: "child machine", result: output })) + } + } } } }, Completed: { on: { - Reset: (to) => - to.branch.Gallery.initial.resolve(({ target }) => target.decoded(new Gallery({ selectedDemo: null }))) + Reset: { initial: targets2.root.Gallery, decoded: () => (new Gallery({ selectedDemo: null })) } } }, Failed: { on: { - Reset: (to) => - to.branch.Gallery.initial.resolve(({ target }) => target.decoded(new Gallery({ selectedDemo: null }))) + Reset: { initial: targets2.root.Gallery, decoded: () => (new Gallery({ selectedDemo: null })) } } } } diff --git a/packages/devtools/src/internal/browser/layout-resilience-example.ts b/packages/devtools/src/internal/browser/layout-resilience-example.ts index 9e6849fc..cd6ef6ea 100644 --- a/packages/devtools/src/internal/browser/layout-resilience-example.ts +++ b/packages/devtools/src/internal/browser/layout-resilience-example.ts @@ -58,7 +58,29 @@ const AuthEvents = Machine.eventsFromSchemas( }) ) +const targets1 = Machine.targets(AuthStates) export const layoutResilienceMachine = Machine.make({ + branches: { + transition4: { + requestVerification: { + target: targets1.root.Editing.RequestingVerification, + title: "Request a verification code" + }, + login: { target: targets1.root.Editing.SubmittingLogin, title: "Submit password login" }, + invalid: { target: targets1.root.Editing.Form.Failed, title: "Show validation failure" } + } + }, + effects: { + source1: ({ containingState }: { readonly containingState: { readonly email: string } }) => + containingState.email === "fail" + ? Effect.fail("invalid credentials") + : Effect.succeed("/creator"), + source2: ({ containingState }: { readonly containingState: { readonly email: string } }) => + containingState.email === "fail" + ? Effect.fail("verification unavailable") + : Effect.succeed(undefined) + }, + id: "layout-resilience", root: AuthStates, events: AuthEvents, @@ -79,37 +101,26 @@ export const layoutResilienceMachine = Machine.make({ states: { Form: { on: { - EmailChanged: (to) => - to.branch.Editing.update.resolve(({ current, event, owner }) => - owner.from({ ...current, email: event.value }) - ), - LoginMethodChanged: (to) => - to.branch.Editing.update.resolve(({ current, event, owner }) => - owner.from({ ...current, loginMethod: event.value }) - ), - PasswordChanged: (to) => - to.branch.Editing.update.resolve(({ current, event, owner }) => - owner.from({ ...current, password: event.value }) - ), - Submit: (to) => - to.branches({ - requestVerification: { - title: "Request a verification code", - target: to.branch.Editing.RequestingVerification() - }, - login: { - title: "Submit password login", - target: to.branch.Editing.SubmittingLogin() - }, - invalid: { - title: "Show validation failure", - target: to.local.Failed() - } - }).resolve(({ event, select }) => { + EmailChanged: { + update: targets1.root.Editing, + from: ({ ancestors: { "Editing": current }, event }) => ({ ...current, email: event.value }) + }, + LoginMethodChanged: { + update: targets1.root.Editing, + from: ({ ancestors: { "Editing": current }, event }) => ({ ...current, loginMethod: event.value }) + }, + PasswordChanged: { + update: targets1.root.Editing, + from: ({ ancestors: { "Editing": current }, event }) => ({ ...current, password: event.value }) + }, + Submit: { + branches: "transition4", + resolve: ({ event, select }) => { if (event.route === "login") return select.login.from() if (event.route === "verification") return select.requestVerification.from() return select.invalid.from({ message: "Enter valid authentication details." }) - }) + } + } }, states: { Ready: {}, @@ -117,32 +128,35 @@ export const layoutResilienceMachine = Machine.make({ } }, SubmittingLogin: { - invoke: (from) => - from.effect("submit-login", ({ containingState }) => - containingState.email === "fail" - ? Effect.fail("invalid credentials") - : Effect.succeed("/creator")).onDone((to) => - to.branch.Navigating().resolve(({ output, target }) => target.from({ href: output }))).onFailure((to) => - to.branch.Editing.Form.Failed().resolve(({ target }) => - target.from({ message: "Email or password is incorrect." }) - )) + invoke: { + src: "source1", + id: "submit-login", + input: (context) => context, + onDone: { target: targets1.root.Navigating, from: ({ output }) => ({ href: output }) }, + onFailure: { + target: targets1.root.Editing.Form.Failed, + from: () => ({ message: "Email or password is incorrect." }) + } + } }, RequestingVerification: { - invoke: (from) => - from.effect("request-verification", ({ containingState }) => - containingState.email === "fail" - ? Effect.fail("verification unavailable") - : Effect.succeed(undefined)).onDone((to) => - to.branch.Verification.initial.resolve(({ containingState, target }) => - target.from({ - mode: containingState.mode, - email: containingState.email, - code: "" - }) - )).onFailure((to) => - to.branch.Editing.Form.Failed().resolve(({ target }) => - target.from({ message: "The verification code could not be sent." }) - )) + invoke: { + src: "source2", + id: "request-verification", + input: (context) => context, + onDone: { + initial: targets1.root.Verification, + from: ({ containingState }) => ({ + mode: containingState.mode, + email: containingState.email, + code: "" + }) + }, + onFailure: { + target: targets1.root.Editing.Form.Failed, + from: () => ({ message: "The verification code could not be sent." }) + } + } } } }, diff --git a/packages/devtools/src/internal/browser/parallel-completion-example.ts b/packages/devtools/src/internal/browser/parallel-completion-example.ts index 3b36d898..d9f9a936 100644 --- a/packages/devtools/src/internal/browser/parallel-completion-example.ts +++ b/packages/devtools/src/internal/browser/parallel-completion-example.ts @@ -95,7 +95,10 @@ const ParallelStates = Machine.state({ } }) +const targets1 = Machine.targets(ParallelStates) export const parallelCompletionMachine = Machine.make({ + timers: { source1: "5 seconds" }, + id: "parallel-completion", root: ParallelStates, events: Machine.eventsFromSchemas( @@ -115,40 +118,39 @@ export const parallelCompletionMachine = Machine.make({ states: { Cart: { on: { - Checkout: (to) => - to.branch.Order.initial.resolve(({ event, target }) => - target.decoded(new Order({ orderId: event.orderId, total: event.total })) - ) + Checkout: { + initial: targets1.root.Order, + decoded: ({ event }) => (new Order({ orderId: event.orderId, total: event.total })) + } } }, Order: { initialize: ({ builder }) => builder.payment.from({ attempts: 0 }).fulfillment.from({ warehouse: "north" }), on: { - CancelOrder: (to) => - to.branch.Cancelled().resolve(({ event, target }) => - target.decoded(new OrderCancelled({ reason: event.reason })) - ) + CancelOrder: { + target: targets1.root.Cancelled, + decoded: ({ event }) => (new OrderCancelled({ reason: event.reason })) + } }, - onDone: (to) => - to.branch.Complete().resolve(({ target }) => target.decoded(new OrderComplete({ orderId: "completed-order" }))), + onDone: { target: targets1.root.Complete, decoded: () => (new OrderComplete({ orderId: "completed-order" })) }, states: { payment: { initialize: ({ builder }) => builder.from(), states: { AwaitingAuthorization: { on: { - Authorize: (to) => - to.local.Authorized().resolve(({ event, target }) => - target.decoded(new Authorized({ authorizationId: event.authorizationId })) - ), - CompleteAll: (to) => - to.local.Authorized().resolve(({ event, target }) => - target.decoded(new Authorized({ authorizationId: event.authorizationId })) - ), - DeclinePayment: (to) => - to.branch.Cancelled().resolve(({ event, target }) => - target.decoded(new OrderCancelled({ reason: event.reason })) - ) + Authorize: { + target: targets1.root.Order.payment.Authorized, + decoded: ({ event }) => (new Authorized({ authorizationId: event.authorizationId })) + }, + CompleteAll: { + target: targets1.root.Order.payment.Authorized, + decoded: ({ event }) => (new Authorized({ authorizationId: event.authorizationId })) + }, + DeclinePayment: { + target: targets1.root.Cancelled, + decoded: ({ event }) => (new OrderCancelled({ reason: event.reason })) + } } } } @@ -158,38 +160,44 @@ export const parallelCompletionMachine = Machine.make({ states: { WaitingForPayment: { on: { - Authorize: (to) => - to.local.Packing().resolve(({ target }) => target.decoded(new Packing({ packageCount: 1 }))), - Pack: (to) => - to.local.Packing().resolve(({ event, target }) => - target.decoded(new Packing({ packageCount: event.packages })) - ), - CompleteAll: (to) => - to.local.Shipped().resolve(({ event, target }) => - target.decoded(new Shipped({ trackingCode: event.trackingCode })) - ) + Authorize: { + target: targets1.root.Order.fulfillment.Packing, + decoded: () => (new Packing({ packageCount: 1 })) + }, + Pack: { + target: targets1.root.Order.fulfillment.Packing, + decoded: ({ event }) => (new Packing({ packageCount: event.packages })) + }, + CompleteAll: { + target: targets1.root.Order.fulfillment.Shipped, + decoded: ({ event }) => (new Shipped({ trackingCode: event.trackingCode })) + } } }, Packing: { - invoke: (from) => - from.timer("packing-sla", "5 seconds").onDone((to) => - to.none.resolve((_, enqueue) => { + invoke: { + src: "source1", + id: "packing-sla", + onDone: { + none: true, + resolve: (_, enqueue) => { enqueue.raise(ParallelInternalEvents.AutoShip()) - }) - ), + } + } + }, on: { - AutoShip: (to) => - to.local.Shipped().resolve(({ target }) => - target.decoded(new Shipped({ trackingCode: "automatic" })) - ), - Ship: (to) => - to.local.Shipped().resolve(({ event, target }) => - target.decoded(new Shipped({ trackingCode: event.trackingCode })) - ), - CompleteAll: (to) => - to.local.Shipped().resolve(({ event, target }) => - target.decoded(new Shipped({ trackingCode: event.trackingCode })) - ) + AutoShip: { + target: targets1.root.Order.fulfillment.Shipped, + decoded: () => (new Shipped({ trackingCode: "automatic" })) + }, + Ship: { + target: targets1.root.Order.fulfillment.Shipped, + decoded: ({ event }) => (new Shipped({ trackingCode: event.trackingCode })) + }, + CompleteAll: { + target: targets1.root.Order.fulfillment.Shipped, + decoded: ({ event }) => (new Shipped({ trackingCode: event.trackingCode })) + } } } } @@ -201,8 +209,7 @@ export const parallelCompletionMachine = Machine.make({ }, Cancelled: { on: { - RetryOrder: (to) => - to.branch.Order.initial.resolve(({ target }) => target.decoded(new Order({ orderId: "retry", total: 0 }))) + RetryOrder: { initial: targets1.root.Order, decoded: () => (new Order({ orderId: "retry", total: 0 })) } } } } diff --git a/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts b/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts index 463722e4..72feeb6d 100644 --- a/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts +++ b/packages/devtools/src/internal/browser/parallel-owner-routing-example.ts @@ -18,6 +18,23 @@ const ChildParentEvents = Machine.eventsFromSchemas(ChildSignal) const ParallelOwnerChildStates = Machine.state({ initial: "Idle", states: { Idle: ChildIdle } }) export const parallelOwnerRoutingChildMachine = Machine.make({ + effects: { + source1: ({ + parent + }: Machine.Machine.InvokeContext< + { + readonly "": { readonly initial: "Idle"; readonly states: { readonly Idle: typeof ChildIdle } } & { + readonly "~effect/Machine/ExplicitInitial": true + } + }, + readonly [], + readonly [], + "Idle", + readonly [], + Machine.Machine.ParentEventSchemas<"required", readonly [typeof ChildSignal]> + >) => parent.send(ChildParentEvents.ChildSignal()) + }, + id: "parallel-owner-routing-child", root: ParallelOwnerChildStates, events: Machine.eventsFromSchemas(), @@ -26,11 +43,13 @@ export const parallelOwnerRoutingChildMachine = Machine.make({ }).handle({ states: { Idle: { - invoke: (from) => - from.effect( - "signal-parent", - ({ parent }) => parent.send(ChildParentEvents.ChildSignal()) - ).onDone((to) => to.none).onFailure((to) => to.none) + invoke: { + src: "source1", + id: "signal-parent", + input: (context) => context, + onDone: { none: true }, + onFailure: { none: true } + } } } }) @@ -64,7 +83,10 @@ const ParallelOwnerStates = Machine.state({ } }) +const targets2 = Machine.targets(ParallelOwnerStates) export const parallelOwnerRoutingMachine = Machine.make({ + children: { source1: ParallelOwnerChild }, + id: "parallel-owner-routing", root: ParallelOwnerStates, events: Machine.eventsFromSchemas(PrintRequested, ChildParentEvents), @@ -83,13 +105,12 @@ export const parallelOwnerRoutingMachine = Machine.make({ states: { Print: { initialize: ({ builder }) => builder.Options.from({}).Operation.from({}), - invoke: (from) => - from.child(ParallelOwnerChild).onFailure((to) => - to.branch.Print.Operation.Active().resolve(({ target }) => target.decoded(new Active({}))) - ), + invoke: { + src: "source1", + onFailure: { target: targets2.root.Print.Operation.Active, decoded: () => (new Active({})) } + }, on: { - PrintRequested: (to) => - to.branch.Print.Operation.Printing().resolve(({ target }) => target.decoded(new Printing({}))) + PrintRequested: { target: targets2.root.Print.Operation.Printing, decoded: () => (new Printing({})) } }, states: { Options: { diff --git a/packages/devtools/src/internal/browser/planner-example.ts b/packages/devtools/src/internal/browser/planner-example.ts index 3dad60e1..af54a45e 100644 --- a/packages/devtools/src/internal/browser/planner-example.ts +++ b/packages/devtools/src/internal/browser/planner-example.ts @@ -76,7 +76,16 @@ const States = Machine.state({ } }) +const targets1 = Machine.targets(States) export const plannerMachine = Machine.make({ + branches: { + transition1: { + urgent: { target: targets1.root.Working, title: "Finish immediately" }, + normal: { target: targets1.root.Working, title: "Wait in working" } + } + }, + effects: { source1: Effect.suspend(() => Effect.never) }, + id: "planner-example", root: States, events: Events, @@ -118,11 +127,9 @@ export const plannerMachine = Machine.make({ states: { Idle: { on: { - Begin: (to) => - to.branches({ - urgent: { title: "Finish immediately", target: to.branch.Working() }, - normal: { title: "Wait in working", target: to.branch.Working() } - }).resolve(({ event, self, select, state }, enqueue) => { + Begin: { + branches: "transition1", + resolve: ({ event, self, select, state }, enqueue) => { enqueue.emit(new Planned({ job: event.job })) enqueue.sendTo(self, Events.Cancel({ reason: "planner command example" })) if (event.priority === "urgent") enqueue.raise(InternalEvents.AutoFinish()) @@ -130,18 +137,18 @@ export const plannerMachine = Machine.make({ return event.priority === "urgent" ? select.urgent.decoded(working) : select.normal.decoded(working) - }) + } + } } }, Working: { - invoke: (from) => from.effect("monitor-job", () => Effect.never), + invoke: { src: "source1", id: "monitor-job" }, on: { - AutoFinish: (to) => - to.branch.Finished().resolve(({ state, target }) => target.decoded(new Finished({ job: state.job }))), - Cancel: (to) => - to.branch.Idle().resolve(({ event, state, target }) => - target.decoded(new Idle({ owner: `${state.owner} · ${event.reason}` })) - ) + AutoFinish: { target: targets1.root.Finished, decoded: ({ state }) => (new Finished({ job: state.job })) }, + Cancel: { + target: targets1.root.Idle, + decoded: ({ event, state }) => (new Idle({ owner: `${state.owner} · ${event.reason}` })) + } } }, Finished: { diff --git a/packages/devtools/src/internal/browser/protocol-events-example.ts b/packages/devtools/src/internal/browser/protocol-events-example.ts index 8f6ccad7..b9e24bf2 100644 --- a/packages/devtools/src/internal/browser/protocol-events-example.ts +++ b/packages/devtools/src/internal/browser/protocol-events-example.ts @@ -56,7 +56,21 @@ const ChildStates = Machine.state({ } }) +const targets1 = Machine.targets(ChildStates) export const requiredParentChildMachine = Machine.make({ + branches: { + transition1: { destination: { target: targets1.root.Working } }, + transition3: { destination: { target: targets1.root.Working } } + }, + effects: { + source1: ( + { parent, state }: { + readonly parent: Machine.MachineTarget> + readonly state: ChildWorking + } + ) => parent.send(ParentEvents.ChildProgress({ percent: state.progress })) + }, + id: "required-parent-child", root: ChildStates, events: ChildEvents, @@ -68,38 +82,43 @@ export const requiredParentChildMachine = Machine.make({ states: { Idle: { on: { - BeginChildWork: (to) => - to.branch.Working().resolve(({ event, target }, enqueue) => { + BeginChildWork: { + branches: "transition1", + 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 })) - }) + } + } } }, Working: { - invoke: (from) => - from.effect( - "report-progress-to-parent", - ({ parent, state }) => parent.send(ParentEvents.ChildProgress({ percent: state.progress })) - ).onDone((to) => to.none).onFailure((to) => - to.branch.Cancelled().resolve(({ error, target }) => - target.decoded(new ChildCancelled({ reason: String(error) })) - ) - ), + invoke: { + src: "source1", + id: "report-progress-to-parent", + input: (context) => context, + onDone: { none: true }, + onFailure: { + target: targets1.root.Cancelled, + decoded: ({ error }) => (new ChildCancelled({ reason: String(error) })) + } + }, on: { - Heartbeat: (to) => - to.branch.Working().resolve(({ event, state, target }, enqueue) => { + Heartbeat: { + branches: "transition3", + resolve: ({ event, state, select: { destination: target } }, enqueue) => { enqueue.raise(ChildInternalEvents.CommitChildWork()) return target.decoded(new ChildWorking({ job: state.job, progress: event.percent })) - }), - CommitChildWork: (to) => - to.branch.Done().resolve(({ state, target }) => - target.decoded(new ChildDone({ result: `${state.job}:complete` })) - ), - CancelChildWork: (to) => - to.branch.Cancelled().resolve(({ event, target }) => - target.decoded(new ChildCancelled({ reason: event.reason })) - ) + } + }, + CommitChildWork: { + target: targets1.root.Done, + decoded: ({ state }) => (new ChildDone({ result: `${state.job}:complete` })) + }, + CancelChildWork: { + target: targets1.root.Cancelled, + decoded: ({ event }) => (new ChildCancelled({ reason: event.reason })) + } } }, Done: { @@ -136,7 +155,14 @@ const ParentStates = Machine.state({ } }) +const targets2 = Machine.targets(ParentStates) export const parentProtocolMachine = Machine.make({ + branches: { + transition1: { destination: { target: targets2.root.Supervising } }, + transition3: { destination: { target: targets2.root.Complete } } + }, + children: { source1: ProtocolChild }, + id: "parent-child-protocol", root: ParentStates, events: Machine.eventsFromSchemas(LaunchChild, ResetParent, ParentEvents), @@ -145,33 +171,32 @@ export const parentProtocolMachine = Machine.make({ states: { Idle: { on: { - LaunchChild: (to) => - to.branch.Supervising().resolve(({ target }) => target.decoded(new Supervising({ latestProgress: 0 }))) + LaunchChild: { target: targets2.root.Supervising, decoded: () => (new Supervising({ latestProgress: 0 })) } } }, Supervising: { - invoke: (from) => - from.child(ProtocolChild).onDone((to) => - to.branch.Complete().resolve(({ output, target }) => target.decoded(new ParentComplete({ result: output }))) - ).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new ParentFailed({ message: String(error) })) - ) - ), + invoke: { + src: "source1", + onDone: { target: targets2.root.Complete, decoded: ({ output }) => (new ParentComplete({ result: output })) }, + onFailure: { + target: targets2.root.Failed, + decoded: ({ error }) => (new ParentFailed({ message: String(error) })) + } + }, on: { - ChildProgress: (to) => - to.branch.Supervising().resolve(({ event, target }) => - target.decoded(new Supervising({ latestProgress: event.percent })) - ), - ChildFinished: (to) => - to.branch.Complete().resolve(({ event, target }) => - target.decoded(new ParentComplete({ result: event.result })) - ), - ChildProblem: (to) => - to.branch.Failed().resolve(({ event, target }) => - target.decoded(new ParentFailed({ message: event.message })) - ), - ResetParent: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new ParentIdle({}))) + ChildProgress: { + target: targets2.root.Supervising, + decoded: ({ event }) => (new Supervising({ latestProgress: event.percent })) + }, + ChildFinished: { + target: targets2.root.Complete, + decoded: ({ event }) => (new ParentComplete({ result: event.result })) + }, + ChildProblem: { + target: targets2.root.Failed, + decoded: ({ event }) => (new ParentFailed({ message: event.message })) + }, + ResetParent: { target: targets2.root.Idle, decoded: () => (new ParentIdle({})) } } }, Complete: { @@ -179,7 +204,7 @@ export const parentProtocolMachine = Machine.make({ }, Failed: { on: { - ResetParent: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new ParentIdle({}))) + ResetParent: { target: targets2.root.Idle, decoded: () => (new ParentIdle({})) } } } } @@ -196,7 +221,10 @@ class PublishOutside extends Schema.TaggedClass("OptionalParentP const OptionalParentStates = Machine.state({ initial: "Detached", states: { Detached, Published } }) +const targets3 = Machine.targets(OptionalParentStates) export const optionalParentMachine = Machine.make({ + branches: { transition1: { destination: { target: targets3.root.Published } } }, + id: "optional-parent-protocol", root: OptionalParentStates, events: Machine.eventsFromSchemas(PublishOutside), @@ -207,13 +235,15 @@ export const optionalParentMachine = Machine.make({ states: { Detached: { on: { - PublishOutside: (to) => - to.branch.Published().resolve(({ event, parent, target }, enqueue) => { + PublishOutside: { + branches: "transition1", + resolve: ({ event, parent, select: { destination: target } }, enqueue) => { if (parent !== undefined) { enqueue.sendTo(parent, ParentEvents.ChildFinished({ result: event.result })) } return target.decoded(new Published({ deliveredToParent: parent !== undefined })) - }) + } + } } }, Published: {} diff --git a/packages/devtools/src/internal/browser/shared-terminal-routing-example.ts b/packages/devtools/src/internal/browser/shared-terminal-routing-example.ts index 8eff5fdb..6fa8cfe5 100644 --- a/packages/devtools/src/internal/browser/shared-terminal-routing-example.ts +++ b/packages/devtools/src/internal/browser/shared-terminal-routing-example.ts @@ -28,9 +28,22 @@ const ReplicationEvents = Machine.eventsFromSchemas( }) ) -const operation = (): Effect.Effect => Effect.succeed(undefined) +const operation: Effect.Effect = Effect.succeed(undefined) +const targets1 = Machine.targets(ReplicationStates) export const sharedTerminalRoutingMachine = Machine.make({ + effects: { + source1: operation, + source2: operation, + source3: operation, + source4: operation, + source5: operation, + source6: operation, + source7: operation, + source8: operation, + source9: operation + }, + id: "shared-terminal-routing", root: ReplicationStates, events: ReplicationEvents, @@ -38,108 +51,126 @@ export const sharedTerminalRoutingMachine = Machine.make({ }).handle({ states: { Connecting: { - invoke: (from) => - from.effect("connect", operation) - .onDone((to) => to.branch.IdentifyingSource()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source1", + id: "connect", + onDone: { target: targets1.root.IdentifyingSource }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, IdentifyingSource: { - invoke: (from) => - from.effect("identify-source", operation) - .onDone((to) => to.branch.ReadingServerInfo()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source2", + id: "identify-source", + onDone: { target: targets1.root.ReadingServerInfo }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, ReadingServerInfo: { - invoke: (from) => - from.effect("read-server-info", operation) - .onDone((to) => to.branch.ReadingSlot()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source3", + id: "read-server-info", + onDone: { target: targets1.root.ReadingSlot }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, ReadingSlot: { - invoke: (from) => - from.effect("read-slot", operation) - .onDone((to) => to.branch.CreatingSlot()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source4", + id: "read-slot", + onDone: { target: targets1.root.CreatingSlot }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, CreatingSlot: { - invoke: (from) => - from.effect("create-slot", operation) - .onDone((to) => to.branch.CopyingSnapshot()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source5", + id: "create-slot", + onDone: { target: targets1.root.CopyingSnapshot }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, CopyingSnapshot: { - invoke: (from) => - from.effect("copy-snapshot", operation) - .onDone((to) => to.branch.CatchingUp()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source6", + id: "copy-snapshot", + onDone: { target: targets1.root.CatchingUp }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, CatchingUp: { - invoke: (from) => - from.effect("catch-up", operation) - .onDone((to) => to.branch.ApplyingChanges()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source7", + id: "catch-up", + onDone: { target: targets1.root.ApplyingChanges }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, ApplyingChanges: { - invoke: (from) => - from.effect("apply-changes", operation) - .onDone((to) => to.branch.Ready()) - .onFailure((to) => to.branch.Failed()), + invoke: { + src: "source8", + id: "apply-changes", + onDone: { target: targets1.root.Ready }, + onFailure: { target: targets1.root.Failed } + }, on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, Ready: { on: { - SessionUnavailable: (to) => to.branch.SessionUnavailable(), - StopRequested: (to) => to.branch.Stopping() + SessionUnavailable: { target: targets1.root.SessionUnavailable }, + StopRequested: { target: targets1.root.Stopping } } }, SessionUnavailable: { on: { - Retry: (to) => to.branch.Connecting(), - StopRequested: (to) => to.branch.Stopping() + Retry: { target: targets1.root.Connecting }, + StopRequested: { target: targets1.root.Stopping } } }, Stopping: { - invoke: (from) => - from.effect("stop-session", operation) - .onDone((to) => to.branch.Stopped()) - .onFailure((to) => to.branch.Failed()) + invoke: { + src: "source9", + id: "stop-session", + onDone: { target: targets1.root.Stopped }, + onFailure: { target: targets1.root.Failed } + } }, Stopped: {}, Failed: { on: { - Retry: (to) => to.branch.Connecting(), - StopRequested: (to) => to.branch.Stopping() + Retry: { target: targets1.root.Connecting }, + StopRequested: { target: targets1.root.Stopping } } } } diff --git a/packages/devtools/src/internal/browser/transition-semantics-example.ts b/packages/devtools/src/internal/browser/transition-semantics-example.ts index e0b3953f..ceafbccf 100644 --- a/packages/devtools/src/internal/browser/transition-semantics-example.ts +++ b/packages/devtools/src/internal/browser/transition-semantics-example.ts @@ -117,7 +117,21 @@ const defaultWorkspaceSnapshot = () => ({ } }) +const targets1 = Machine.targets(TransitionStates) 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" } + }, + transition7: { + review: { initial: 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 } }, + transition15: { destination: { history: targets1.root.Workspace.exact } } + }, + id: "transition-semantics", root: TransitionStates, events: Machine.eventsFromSchemas( @@ -148,87 +162,80 @@ export const transitionSemanticsMachine = Machine.make({ exact: { default: defaultWorkspaceSnapshot } }, on: { - Pause: (to) => - to.branch.Paused().resolve(({ event, target }) => target.decoded(new Paused({ reason: event.reason }))), - BumpWorkspace: (to) => - to.branch.Workspace.update.resolve(({ current, owner }) => - owner.decoded( - new Workspace({ - revision: current.revision + 1, - preferredRoute: current.preferredRoute - }) - ) - ) + Pause: { target: targets1.root.Paused, decoded: ({ event }) => (new Paused({ reason: event.reason })) }, + BumpWorkspace: { + update: targets1.root.Workspace, + decoded: ({ state: current }) => (new Workspace({ + revision: current.revision + 1, + preferredRoute: current.preferredRoute + })) + } }, - onDone: (to) => - to.branch.Published().resolve(({ target }) => target.decoded(new Published({ result: "workspace published" }))), + onDone: { target: targets1.root.Published, decoded: () => (new Published({ result: "workspace published" })) }, states: { Routing: { - choice: (to) => - to.branches({ - draft: { title: "Preferred route is draft", target: to.local.Draft() }, - review: { title: "Preferred route is review", target: to.local.Review.initial } - }).resolve(({ containingState, select }) => + choice: { + branches: "transition4", + resolve: ({ containingState, select }) => containingState.preferredRoute === "review" ? select.review.decoded(new Review({ requestedBy: "initial route" })) : select.draft.decoded(new Draft({ text: "", autosaves: 0 })) - ) + } }, Draft: { on: { - Edit: (to) => - to.local.Draft().resolve(({ event, state, target }) => - target.decoded(new Draft({ text: event.text, autosaves: state.autosaves })) - ), - Save: (to) => to.local.AutoSaving().resolve(({ target }) => target.decoded(new AutoSaving({}))), - Submit: (to) => - to.branches({ - review: { title: "Enter the review flow", target: to.local.Review.initial }, - publish: { title: "Publish without review", target: to.local.Finished() } - }).resolve(({ event, select }) => + Edit: { + target: targets1.root.Workspace.Draft, + decoded: ({ event, state }) => (new Draft({ text: event.text, autosaves: state.autosaves })) + }, + Save: { target: targets1.root.Workspace.AutoSaving, decoded: () => (new AutoSaving({})) }, + Submit: { + branches: "transition7", + resolve: ({ event, select }) => event.mode === "publish" ? select.publish.decoded(new WorkspaceFinished({ result: "published directly" })) : select.review.decoded(new Review({ requestedBy: event.requestedBy })) - ), - Refresh: (to) => to.none.reenter(), - Ignore: (to) => to.none, - MaybeHandle: (to) => - to.none.resolve(({ decline, event }) => event.accept ? undefined : decline(), { - declinable: true - }) + }, + Refresh: { none: true, reenter: true }, + Ignore: { none: true }, + MaybeHandle: { + none: true, + resolve: ({ decline, event }) => event.accept ? undefined : decline(), + declinable: true + } } }, AutoSaving: { - always: (to) => - to.local.Draft().resolve(({ target }) => - target.decoded(new Draft({ text: "Autosaved draft", autosaves: 1 })) - ) + always: { + target: targets1.root.Workspace.Draft, + decoded: () => (new Draft({ text: "Autosaved draft", autosaves: 1 })) + } }, Review: { initialize: ({ builder }) => builder.decoded(new Checking({ checks: ["types", "tests"] })), - onDone: (to) => - to.branch.Workspace.Finished().resolve(({ target }) => - target.decoded(new WorkspaceFinished({ result: "approved review" })) - ), + onDone: { + target: targets1.root.Workspace.Finished, + decoded: () => (new WorkspaceFinished({ result: "approved review" })) + }, states: { Checking: { on: { - Approve: (to) => - to.local.Approved().resolve(({ event, target }) => - target.decoded(new Approved({ reviewer: event.reviewer })) - ), - Reject: (to) => - to.local.ChangesRequested().resolve(({ event, target }) => - target.decoded(new ChangesRequested({ reason: event.reason })) - ) + Approve: { + target: targets1.root.Workspace.Review.Approved, + decoded: ({ event }) => (new Approved({ reviewer: event.reviewer })) + }, + Reject: { + target: targets1.root.Workspace.Review.ChangesRequested, + decoded: ({ event }) => (new ChangesRequested({ reason: event.reason })) + } } }, ChangesRequested: { on: { - Revise: (to) => - to.branch.Workspace.Draft().resolve(({ target }) => - target.decoded(new Draft({ text: "Revised draft", autosaves: 0 })) - ) + Revise: { + target: targets1.root.Workspace.Draft, + decoded: () => (new Draft({ text: "Revised draft", autosaves: 0 })) + } } } } @@ -237,16 +244,16 @@ export const transitionSemanticsMachine = Machine.make({ }, Paused: { on: { - Create: (to) => - to.branch.Workspace.initial.resolve(({ event, target }) => - target.decoded(new Workspace({ revision: 0, preferredRoute: event.route })) - ), - ResumeShallow: (to) => to.history.Workspace.recent.resolve(({ target }) => target()), - ResumeDeep: (to) => to.history.Workspace.exact.resolve(({ target }) => target()), - Restart: (to) => - to.branch.Workspace.initial.resolve(({ target }) => - target.decoded(new Workspace({ revision: 0, preferredRoute: "draft" })) - ) + Create: { + initial: targets1.root.Workspace, + decoded: ({ 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, + decoded: () => (new Workspace({ revision: 0, preferredRoute: "draft" })) + } } }, Disabled: {}, diff --git a/packages/devtools/src/internal/browser/visualizer-app.ts b/packages/devtools/src/internal/browser/visualizer-app.ts index 108b78a0..be92516d 100644 --- a/packages/devtools/src/internal/browser/visualizer-app.ts +++ b/packages/devtools/src/internal/browser/visualizer-app.ts @@ -76,8 +76,6 @@ const stateLink = (path: string, label: string, navigate: StateNavigator): HTMLB } const renderBranch = ( - document: VisualizationDocument, - source: string, branch: VisualizationBranch, navigate: StateNavigator ): HTMLElement => { @@ -88,7 +86,7 @@ const renderBranch = ( main.append(createElement("span", "branch-arrow", "→"), stateLink(branch.target, branch.target, navigate)) } else if (branch.selection.kind === "update") { main.append( - badge(branch.selection.scope === "local" ? "to.local.update" : "value update", "update"), + badge("value update", "update"), createElement("span", "branch-target", "Updates the owner value") ) } else { @@ -96,7 +94,7 @@ const renderBranch = ( } row.append(main) - const api = branchTargetApi(document, source, branch) + const api = branchTargetApi(branch) const details: Array = api === undefined ? [["Selection", branch.selection.kind], ["Scope", branch.selection.scope]] : [["API", api]] @@ -111,7 +109,6 @@ const renderBranch = ( } const renderTransition = ( - document: VisualizationDocument, transition: VisualizationTransition, navigate: StateNavigator, showSource = false @@ -134,16 +131,13 @@ const renderTransition = ( if (transition.branches.length > 0) { const branches = createElement("div", "branch-list") - transition.branches.forEach((branch) => - branches.append(renderBranch(document, transition.source, branch, navigate)) - ) + transition.branches.forEach((branch) => branches.append(renderBranch(branch, navigate))) card.append(branches) } return card } const renderIncomingTransition = ( - document: VisualizationDocument, incoming: IncomingTransition, navigate: StateNavigator ): HTMLElement => { @@ -156,7 +150,7 @@ const renderIncomingTransition = ( header.append(title, flags) card.append(header) - const api = branchTargetApi(document, incoming.transition.source, incoming.branch) + const api = branchTargetApi(incoming.branch) const details: Array = [ ["Source", stateLink(incoming.transition.source, incoming.transition.source, navigate)], ["API", api] @@ -576,16 +570,14 @@ export const renderVisualizer = ( if (inspection.outgoing.length > 0) { const transitions = createElement("section", "inspector-section") transitions.append(inspectionSection("Transitions", inspection.outgoing.length)) - inspection.outgoing.forEach((transition) => - transitions.append(renderTransition(visualization, transition, navigateToState)) - ) + inspection.outgoing.forEach((transition) => transitions.append(renderTransition(transition, navigateToState))) inspectorContent.append(transitions) } if (inspection.incoming.length > 0) { const incoming = createElement("section", "inspector-section") incoming.append(inspectionSection("Entered by", inspection.incoming.length)) inspection.incoming.forEach((transition) => - incoming.append(renderIncomingTransition(visualization, transition, navigateToState)) + incoming.append(renderIncomingTransition(transition, navigateToState)) ) inspectorContent.append(incoming) } @@ -623,9 +615,7 @@ export const renderVisualizer = ( const details = createElement("section", "inspector-section") details.append(inspectionSection("Branches", transition.branches.length)) const branches = createElement("div", "inspection-card branch-list") - transition.branches.forEach((branch) => - branches.append(renderBranch(visualization, transition.source, branch, navigateToState)) - ) + transition.branches.forEach((branch) => branches.append(renderBranch(branch, navigateToState))) details.append(branches) inspectorContent.append(details) } @@ -758,7 +748,7 @@ export const renderVisualizer = ( const choice = frame.choice const transition = transitionsById.get(choice.transitionId) const branch = transition?.branches.find(({ id }) => id === choice.branchId) - const api = branch === undefined ? undefined : branchTargetApi(visualization, choice.source, branch) + const api = branch === undefined ? undefined : branchTargetApi(branch) const selection = createElement("section", "inspector-section") selection.append(inspectionSection("Selected branch")) const card = createElement("article", "inspection-card trace-card") diff --git a/packages/devtools/src/internal/browser/visualizer-model.ts b/packages/devtools/src/internal/browser/visualizer-model.ts index 749170a4..daaff1ff 100644 --- a/packages/devtools/src/internal/browser/visualizer-model.ts +++ b/packages/devtools/src/internal/browser/visualizer-model.ts @@ -76,104 +76,19 @@ const propertyAccess = (key: string): string => /^[$A-Z_a-z][$\w]*$/.test(key) ? const pathAccess = (path: string): string => path === "" ? "" : path.split(".").map(propertyAccess).join("") -const nearestCompoundScope = ( - document: VisualizationDocument, - source: string -): string | undefined => { - const states = new Map(document.states.map((state) => [state.path, state])) - let current = states.get(source) - while (current !== undefined) { - if (current.type === "compound") return current.path - current = current.parent === null ? undefined : states.get(current.parent) - } - return undefined -} - -const localPathAccess = ( - document: VisualizationDocument, - source: string, - path: string -): string | undefined => { - const scope = nearestCompoundScope(document, source) - if (scope === undefined) return undefined - if (path === scope) return "" - const prefix = scope === "" ? "" : `${scope}.` - return path.startsWith(prefix) ? pathAccess(path.slice(prefix.length)) : undefined -} - -/** Canonical Effect Machine selector represented by one retained transition branch. */ -export const branchTargetApi = ( - document: VisualizationDocument, - source: string, - branch: VisualizationBranch -): string | undefined => { +/** Public declaration represented by a retained transition branch. */ +export const branchTargetApi = (branch: VisualizationBranch): string | undefined => { const { selection } = branch - if (selection.kind === "none") return "to.none" + if (selection.kind === "none") return "{ none: true }" const path = selection.path - if (path === null || selection.scope === null) return undefined - - let api: string | undefined - if (selection.kind === "history" && selection.scope === "full") { - api = `to.history${pathAccess(path)}` - } else if (selection.scope === "local") { - const local = localPathAccess(document, source, path) - if (local === undefined) return undefined - switch (selection.kind) { - case "state": - api = local === "" ? "to.local.with" : `to.local${local}()` - break - case "choice": - api = local === "" ? undefined : `to.local${local}()` - break - case "initial": - api = local === "" ? undefined : `to.local${local}.initial` - break - case "update": - api = local === "" ? "to.local.update" : undefined - break - case "history": - break - } - } else if (selection.scope === "branch") { - switch (selection.kind) { - case "state": - case "choice": - api = path === "" ? "to.root()" : `to.branch${pathAccess(path)}()` - break - case "initial": - api = path === "" ? "to.root.initial" : `to.branch${pathAccess(path)}.initial` - break - case "update": - api = path === "" ? "to.root.update" : `to.branch${pathAccess(path)}.update` - break - case "history": - break - } - } else if (selection.scope === "full") { - switch (selection.kind) { - case "state": - case "choice": - api = `to.full${pathAccess(path)}()` - break - case "initial": - api = `to.full${pathAccess(path)}.initial` - break - case "history": - api = `to.history${pathAccess(path)}` - break - case "update": - break - } - } else if (selection.scope === "initial") { - if (selection.kind === "state") api = path === "" ? "to" : `to${pathAccess(path)}()` - if (selection.kind === "initial") api = `to${pathAccess(path)}.initial` + if (path === null) return undefined + if (selection.scope === "initial") return "initialConfiguration" + const operation = selection.kind === "state" || selection.kind === "choice" ? "target" : selection.kind + const fields = [`${operation}: targets.root${pathAccess(path)}`] + if (operation !== "update") { + for (const owner of branch.updates) fields.push(`update: targets.root${pathAccess(owner)}`) } - - if (api === undefined || selection.kind === "update") return api - return branch.updates.reduce( - (expression, owner) => `${expression}.updating(to.branch${pathAccess(owner)})`, - api - ) + return `{ ${fields.join(", ")} }` } const buildInitialPaths = (document: VisualizationDocument): ReadonlySet => { diff --git a/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts b/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts index 8edfe216..1233c239 100644 --- a/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts +++ b/packages/devtools/test/internal/browser/InteractiveTextVisualization.test.ts @@ -34,9 +34,11 @@ describe("Interactive text visualization", () => { acceptance: "required", branches: [{ id: "application.workflow.idle:transition:0:branch:0", - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "application.workflow.running", - selection: { path: "application.workflow.running", kind: "state", scope: "local" }, + selection: { path: "application.workflow.running", kind: "state", scope: "branch" }, updates: ["application.workflow"] }] }) @@ -103,11 +105,11 @@ describe("Interactive text visualization", () => { const document = buildDocument() const plannerDocument = MachineDocument.make(plannerMachine) const api = ( - source: string, + _source: string, selection: MachineDocument.Selection, updates: ReadonlyArray = [] ): string | undefined => - branchTargetApi(document, source, { + branchTargetApi({ id: "test-branch", type: "direct", target: selection.path, @@ -125,33 +127,33 @@ describe("Interactive text visualization", () => { ) assert.deepStrictEqual( - begin?.branches.map((branch) => branchTargetApi(plannerDocument, begin.source, branch)), - ["to.branch.Working()", "to.branch.Working()"] + begin?.branches.map((branch) => branchTargetApi(branch)), + ["{ target: targets.root.Working }", "{ target: targets.root.Working }"] ) assert.strictEqual( - start === undefined ? undefined : branchTargetApi(document, start.source, start.branches[0]!), - "to.local.running().updating(to.branch.application.workflow)" + start === undefined ? undefined : branchTargetApi(start.branches[0]!), + "{ target: targets.root.application.workflow.running, update: targets.root.application.workflow }" ) assert.strictEqual( - refresh === undefined ? undefined : branchTargetApi(document, refresh.source, refresh.branches[0]!), - "to.local.update" + refresh === undefined ? undefined : branchTargetApi(refresh.branches[0]!), + "{ update: targets.root.application.workflow }" ) assert.strictEqual( api("application.workflow.running.editing", { path: "application.workflow.running", kind: "state", - scope: "local" + scope: "branch" }), - "to.local.with" + "{ target: targets.root.application.workflow.running }" ) assert.strictEqual( api("application.workflow.idle", { path: "application.workflow.running", kind: "initial", - scope: "local" + scope: "branch" }), - "to.local.running.initial" + "{ initial: targets.root.application.workflow.running }" ) assert.strictEqual( api("application.workflow.idle", { @@ -159,7 +161,7 @@ describe("Interactive text visualization", () => { kind: "update", scope: "branch" }), - "to.branch.application.workflow.update" + "{ update: targets.root.application.workflow }" ) assert.strictEqual( api("application.workflow.idle", { @@ -167,7 +169,7 @@ describe("Interactive text visualization", () => { kind: "history", scope: "full" }), - "to.history.application.workflow.recent" + "{ history: targets.root.application.workflow.recent }" ) assert.strictEqual( api("application.workflow.idle", { @@ -175,9 +177,9 @@ describe("Interactive text visualization", () => { kind: "none", scope: "local" }), - "to.none" + "{ none: true }" ) - assert.strictEqual(api("application", document.initial.selection), "to") + assert.strictEqual(api("application", document.initial.selection), "initialConfiguration") }) it("accepts an empty partial topology", () => { diff --git a/packages/devtools/test/internal/browser/StaticChart.test.ts b/packages/devtools/test/internal/browser/StaticChart.test.ts index 847f378d..07dc67d8 100644 --- a/packages/devtools/test/internal/browser/StaticChart.test.ts +++ b/packages/devtools/test/internal/browser/StaticChart.test.ts @@ -989,7 +989,10 @@ describe("Static chart", () => { assert.isTrue(layout.regions.some(({ nodePaths }) => nodePaths.includes("Disabled"))) const recent = layout.nodes.find(({ node }) => node.path === "Workspace.recent") - const resume = layout.edges.find((edge) => edge.kind === "transition" && edge.edge.label === "ResumeShallow") + const resume = layout.edges.find((edge) => + edge.kind === "transition" && edge.edge.trigger.type === "event" && edge.edge.trigger.event === "ResumeShallow" && + edge.edge.target === "Workspace.recent" + ) if (recent === undefined || resume?.kind !== "transition") assert.fail("Expected the shallow history transition") const approach = resume.points.at(-2)! const end = resume.points.at(-1)! diff --git a/packages/effect-machine-react/test/MachineAtom.test.tsx b/packages/effect-machine-react/test/MachineAtom.test.tsx index 7d0c2d54..2b446bd5 100644 --- a/packages/effect-machine-react/test/MachineAtom.test.tsx +++ b/packages/effect-machine-react/test/MachineAtom.test.tsx @@ -18,8 +18,9 @@ class Increment extends Schema.TaggedClass("Increment")("Increment", const States = Machine.state({ initial: "Active", states: { Active } }) -const trackedMachine = (onStart: () => void) => - Machine.make({ +const trackedMachine = (onStart: () => void) => { + const targets1 = Machine.targets(States) + return Machine.make({ root: States, events: Machine.eventsFromSchemas(Increment), input: Schema.Number, @@ -32,12 +33,12 @@ const trackedMachine = (onStart: () => void) => states: { Active: { on: { - Increment: (to) => - to.branch.Active().resolve(({ state, target }) => target.decoded(new Active({ value: state.value + 1 }))) + Increment: { target: targets1.root.Active, decoded: ({ state }) => (new Active({ value: state.value + 1 })) } } } } }) +} afterEach(cleanup) diff --git a/packages/effect-machine-react/test/RootContext.test.tsx b/packages/effect-machine-react/test/RootContext.test.tsx index 49ed1048..601dd8d6 100644 --- a/packages/effect-machine-react/test/RootContext.test.tsx +++ b/packages/effect-machine-react/test/RootContext.test.tsx @@ -11,14 +11,16 @@ import { AtomMachine } from "../../effect-machine/src/unstable/reactivity/index. import { createMachineContext, MachineState } from "../src/index.js" const Events = Machine.events({ Increment: {}, Close: {} }) +const root1 = Machine.state({ fields: { count: Schema.Number }, initial: "Open", states: { Open: {}, Closed: {} } }) +const targets1 = Machine.targets(root1) const counter = Machine.make({ - root: Machine.state({ fields: { count: Schema.Number }, initial: "Open", states: { Open: {}, Closed: {} } }), + root: root1, events: Events, input: Schema.Number, initial: (root) => root.from(({ input }) => ({ count: input })) }).handle({ - on: { Increment: (to) => to.self.update.from(({ current }) => ({ count: current.count + 1 })) }, - states: { Open: { on: { Close: (to) => to.local.Closed() } } } + on: { Increment: { update: targets1.root, from: ({ root: current }) => ({ count: current.count + 1 }) } }, + states: { Open: { on: { Close: { target: targets1.root.Closed } } } } }) const Counter = createMachineContext(AtomMachine.factory(counter)) afterEach(cleanup) diff --git a/packages/effect-machine-react/typetest/MachineAtom.tst.ts b/packages/effect-machine-react/typetest/MachineAtom.tst.ts index 465d0cf5..44aeef52 100644 --- a/packages/effect-machine-react/typetest/MachineAtom.tst.ts +++ b/packages/effect-machine-react/typetest/MachineAtom.tst.ts @@ -9,6 +9,7 @@ class Idle extends Schema.TaggedClass("Idle")("Idle", {}) {} class Continue extends Schema.TaggedClass("Continue")("Continue", {}) {} const States = Machine.state({ initial: "Idle", states: { Idle } }) + const machine = Machine.make({ root: States, events: Machine.eventsFromSchemas(Continue), @@ -17,7 +18,7 @@ const machine = Machine.make({ states: { Idle: { on: { - Continue: (to) => to.none + Continue: { none: true } } } } diff --git a/packages/effect-machine/README.md b/packages/effect-machine/README.md index b41c8e68..0169e651 100644 --- a/packages/effect-machine/README.md +++ b/packages/effect-machine/README.md @@ -42,25 +42,25 @@ in lockstep with this package. ## Quick start -Start with events and root-owned data. A data-only machine needs no child states: +Declare the root once, derive its references, then add behavior with event maps: ```ts import { Machine } from "@typeonce/effect-machine" import { Effect, Schema } from "effect" const Root = Machine.state({ fields: { count: Schema.Number } }) +const targets = Machine.targets(Root) const Events = Machine.events({ Increment: { by: Schema.Number } }) -const CounterDefinition = Machine.make({ +const Counter = Machine.make({ root: Root, events: Events, initial: (root) => root.from(() => ({ count: 0 })) -}) -const Counter = CounterDefinition.handle({ +}).handle({ on: { - Increment: (to) => - to.self.update.from(({ current, event }) => ({ - count: current.count + event.by - })) + Increment: { + update: targets.root, + from: ({ root, event }) => ({ count: root.count + event.by }) + } } }) @@ -70,688 +70,267 @@ const program = Effect.scoped(Effect.gen(function*() { })) ``` -`self.update` replaces the owner's complete value without exiting and reentering -it. Its children and scoped work remain active. Add `initial` and `states` to -the same root descriptor when the workflow needs distinct modes; the root data -survives transitions between those children. - -See [the root API guide](./docs/root-api.md) for topology-only machines, -initialization, reusable schemas, guards, migration, and observation contracts. - -`handle` creates a complete implementation boundary. Its result does not -expose `handle`, so all behavior for one machine belongs in the same handler -tree. Reuse the definition when multiple independent implementations are -useful, such as production and testing variants: - -```ts -const ProductionCounter = CounterDefinition.handle(productionHandlers) -const TestingCounter = CounterDefinition.handle(testingHandlers) -``` - -`Machine.start` returns a `MachineRef` with `send`, `state`, `snapshot`, -`changes`, `emissions`, `join`, and `stop`. Sending enqueues an event; observe -`changes` or use the testing probe when work must be causally acknowledged. +Root data is shared state. `update: targets.root` replaces that value while +retaining the active child configuration and its running work. Ordinary +transitions target a descendant; the root itself is not a destination. ## Modeling workflow -1. Define reusable domain schemas where they express a domain boundary. -2. Declare one root with `Machine.state`. Use inline children or mount reusable - descriptors under `states`. `fields` builds a tagged schema; `schema` keeps - an existing schema and its construction behavior. -3. Declare field records with `Machine.events`, `Machine.internalEvents`, and - `Machine.emittedEvents`. Import existing tagged unions or tagged classes with - the corresponding `eventsFromSchemas`, `internalEventsFromSchemas`, or - `emittedEventsFromSchemas` constructor. -4. Pass the root descriptor to `Machine.make({ root, events })`. Supply root - values with `initial`; declared child defaults determine startup topology. -5. Implement root behavior directly in `handle`, with child behavior in its - `states` property. Add runtime, Atom, testing, or Cluster adapters at the - application boundary. - -The root has path `""`; children retain paths such as `Editing` and -`Editing.Form`. Use `Root.path(...)` to validate path literals. Type full -snapshots as `Machine.Snapshot` or `Machine.Snapshot`, -state values as `Machine.Value`, and selected snapshots as -`Machine.SnapshotAt`. - -### Make invalid states unrepresentable - -Treat topology as a domain contract, not as file organization. A parallel state -declares the full Cartesian product of its regions, so use it only when every -combination has a coherent meaning. If one region must inspect another before -entering a state safely, prefer a compound hierarchy that makes the forbidden -combination impossible. `matches` remains useful for views, tests, and genuine -coordination between independent regions; it should not repair an invalid -state product. - -Keep state-scoped Effects beneath the state that guarantees their resources, -and enforce command availability in the machine rather than only by disabling -UI controls. When entering an inactive compound or parallel state's declared -default, select `.initial`; explicitly construct descendants only for a -non-default configuration or a complete replacement of an already-active -parallel root. - -### Construct state through builders - -Use `.from(...)` when constructing a new state from schema make input: - -```ts -target.from({ draft: event.draft }) -``` - -The machine runs these inputs through the state schema while planning. Schema -defaults, transformations, refinements, and tagged-class identity are -therefore preserved, and decode failures remain typed machine failures. This -is the default construction path. - -Use `.decoded(...)` when the value is already a `Schema.Type`: - -```ts -target.decoded(new Ready({ notice: null })) -``` - -The machine still validates the value against the schema's type side. It does -not run encoded-input transformations again. State builders are not callable; -the method name always makes the construction mode visible. +Put data on the lowest state that owns it. Use tagged schemas or `fields` for +state-owned values and `{}` for structural states. Prefer distinct states over +boolean flags that allow impossible combinations. Put shared behavior on the +lowest common ancestor, and reserve parallel states for independent modes. -When sibling states share fields, remove the source discriminator and pass the -remaining fields through the target schema: - -```ts -const handlers = { - Submit: (to) => - to.local.Saving().resolve(({ state, target }) => { - const { _tag: _, ...fields } = state - return target.from({ ...fields, attempt: 1 }) - }) -} -``` +`Machine.state` declares topology. `Machine.make` captures protocols, startup +input, sources, and named branch groups. `.handle` implements behavior in one +nested object and returns an executable machine. One definition can create +independent implementations by calling `.handle` more than once. -Omit `schema` when a state represents control flow but owns no data. Use `{}` -instead of defining an empty tagged schema: +### Construct state values ```ts -const States = Machine.state({ - initial: "Form", +const Root = Machine.state({ + initial: "Idle", states: { - Form: { - initial: "Editing", - states: { - Editing: {}, - Saving - } - } + Idle: {}, + Loading: { fields: { query: Schema.String } }, + Ready: { fields: { items: Schema.Array(Schema.String) } }, + Failed: { fields: { message: Schema.String } } } }) +const targets = Machine.targets(Root) +const Events = Machine.events({ Search: { query: Schema.String }, Reset: {} }) -const definition = Machine.make({ - root: States, - events: Machine.eventsFromSchemas(), - initialConfiguration: (root) => - root.resolve(({ target }) => target.from((to) => to.Form.from((form) => form.Editing.from()))) +const Search = Machine.make({ root: Root, events: Events }).handle({ + states: { + Idle: { + on: { + Search: { + target: targets.root.Loading, + from: ({ event }) => ({ query: event.query }) + } + } + }, + Loading: { on: { Reset: { target: targets.root.Idle } } } + } }) ``` -Schema-less states remain active, targetable, matchable, and visible through -`getSnapshot`, but have no value to read. Their builders expose only `.from`, -their handler `state` is `undefined`, and `get` / `getWithParents` accept only -schema-backed paths. Add a schema later if the state starts owning data. - -Keep data-bearing state schemas together in a named `Schema.TaggedUnion` and -reference its cases from the topology. For a standalone state schema whose -class identity is useful, declare a named `Schema.TaggedClass`. Do not bury -inline `fields` in a `Machine.state` descriptor. +`from` accepts the schema's make input, including defaults and transformations. +`decoded` accepts its decoded value, preserving class instances. The planner +validates both boundaries and reports `MachineSchemaDecodeError`. Omit construction +only when the selected builder supports empty default construction. -Put data on the narrowest state where it is valid. If sibling phases share -data, put it on their compound parent. +A callback receives the specific event and source-state types, plus `root`, +`containingState`, `ancestors`, and the appropriate machine references. A +transition callback also receives the current logical `snapshot`. There is no +unrestricted destination builder in the callback. -### Separate inputs, raised events, and emissions +### Declare advanced and conditional transitions -`events` is the public machine-input protocol. Events raised to the same machine -belong in `internalEvents`. Ephemeral outward notifications have their own -`emittedEvents` protocol: +A resolver that chooses a branch, constructs nested children, or enqueues +commands uses a named group in `make`. The group captures every possible edge +before any resolver executes: ```ts -export const CommandEvent = Machine.eventsFromSchemas( - Schema.TaggedUnion({ Save: {} }) -) -export type PublicCommandEvent = Machine.EventOf -const InternalEvent = Machine.internalEventsFromSchemas( - Schema.TaggedUnion({ - Saved: { id: Schema.String }, - SaveFailed: { message: Schema.String } - }) -) -const Emissions = Machine.emittedEventsFromSchemas( - Schema.TaggedUnion({ - SaveObserved: { id: Schema.String } - }) -) - -const definition = Machine.make({ - root: States, - events: CommandEvent, - internalEvents: InternalEvent, - emittedEvents: Emissions, - initial: (to) => to.Idle() -}) -``` - -Handlers see both protocols. Typed `send`, `Machine.can`, and `Machine.plan` -accept only public events. Event tags must be unique and public/internal tags -must be disjoint. - -Export the descriptor returned by `Machine.events` instead of exporting its -schemas. This keeps the deferred constructors as the standard way to create -events without exposing schema `.make` methods: - -```ts -ref.send(CommandEvent.Save()) -enqueue.raise(InternalEvent.Saved({ id: "entry-1" })) -enqueue.emit(Emissions.SaveObserved({ id: "entry-1" })) -``` - -The returned constructors preserve each schema's make input, including required -fields and constructor defaults. They defer schema construction until delivery, -so invalid values fail planning or the running machine with -`MachineSchemaDecodeError` instead of throwing at the call site. -Schemas with an open discriminator such as `_tag: Schema.String` remain valid -protocols but cannot expose a finite constructor set; pass a complete event -object to `send` or `Machine.plan` for those events. - -`ref.emissions` is a hot `Stream`: it publishes only notifications produced -after subscription, replays nothing, and completes when the machine terminates. -Snapshots remain separate and stateful: `ref.changes` begins with the current -lifecycle snapshot and then follows later changes. Use `Machine.prepare` when -an observer must be installed before initial-entry actions run: - -```ts -const prepared = yield * Machine.prepare(machine) - -yield * prepared.emissions.pipe( - Stream.runForEach(handleEmission), - Effect.forkScoped({ startImmediately: true }) -) - -const ref = yield * prepared.start -``` - -`Machine.start(machine)` remains the one-step convenience for callers that do -not observe startup emissions. Preparation does not retain or replay an -emission: the observer is simply subscribed before initialization begins. - -### Inspect a live machine tree - -`Machine.prepare(machine).inspection` is the operational counterpart to the -domain-facing `changes` and `emissions` streams. It observes the prepared root -and every locally owned child, `Logic` process, Effect, and timer in one total -publication order: - -```ts -const prepared = yield * Machine.prepare(checkout) - -yield * prepared.inspection.pipe( - Stream.runForEach((record) => Console.log(record.sequence, record.subject.id, record._tag)), - Effect.forkScoped({ startImmediately: true }) -) - -const checkoutRef = yield * prepared.start -``` - -For a handled input, the stream may expose values such as: - -```ts -{ _tag: "EventSent", sequence: 2, deliveryId: 0, - subject: { id: "checkout", sessionId: "machine:0", kind: "Machine" }, - source: undefined, target: { id: "checkout", sessionId: "machine:0" }, - event: CheckoutEvents.Submit(), causedBy: undefined } - -{ _tag: "EventProcessed", sequence: 4, macrostepId: 0, - deliveryId: 0, handled: true, configurationChanged: true, - before: { status: "active", state: /* ... */ }, - after: { status: "active", state: /* ... */ }, microsteps: [/* ... */] } -``` - -The closed `Machine.Inspection.Event` union also reports creation, -initialization and startup failure, direct `Logic` state updates, outward -emissions, Effect/timer activity lifecycles, and termination. Records erase -unrelated child protocols to `unknown`; application-level observation remains -typed through each reference's `changes` and `emissions`. - -The stream is hot, non-replayed, never fails, and completes after the root -terminates. Subscribe before `prepared.start` to capture initialization. Local -session ids are unique only inside that prepared ownership tree: `machine:0` -is the root and later ids identify its descendants. They are intentionally not -distributed identities. Cluster placement, routing, and correlation continue -to use Cluster entity, runner, and request identities at the integration -boundary. - -`AtomMachine.inspection(machineAtom)` provides the same root-scoped stream and -starts a fresh atom-backed machine only after its inspection subscription is -installed. - -Invalid event and emission constructions fail the machine with a typed -`MachineSchemaDecodeError`; they do not throw from the constructor call. - -### Send explicitly between machines - -`raise` targets the current machine in the same macrostep. `sendTo` targets a -machine mailbox and is processed later. A machine that requires an owner -declares the subset of parent inputs it may send with `Machine.parent`: - -```ts -const ParentEvents = Machine.eventsFromSchemas(ChildFinished) - -const child = Machine.make({ - root: ChildStates, - events: ChildEvents, - parent: Machine.parent(ParentEvents), - initial: (to) => to.Working() +const machine = Machine.make({ + root: Root, + events: Events, + branches: { + search: { + loading: { target: targets.root.Loading, title: "Query supplied" }, + idle: { target: targets.root.Idle, title: "Empty query" } + } + } }).handle({ states: { - Working: { + Idle: { on: { - Finish: (to) => - to.branch.Done().resolve(({ parent, target }, enqueue) => { - enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) - return target.from() - }) + Search: { + branches: "search", + resolve: ({ event, select }) => + event.query.length > 0 + ? select.loading.from({ query: event.query }) + : select.idle.from() + } } - }, - Done: {} + } } }) - -const Child = Machine.child("worker", child) -const ParentInputs = Machine.eventsFromSchemas(Start, ParentEvents) ``` -`parent` is statically present in every child callback, and root APIs such as -`Machine.start`, `Machine.planInitial`, Atom machines, and Cluster machines -reject this machine. When `Child` is invoked, the parent definition must accept -every declared parent event; otherwise `.handle(...)` is a compile-time error. -Inside the child, the parent target accepts only those declared events. - -Use `parent: Machine.optionalParent(ParentEvents)` when the same machine is -intentionally valid both as a root and as a child. In that case `parent` is -`MachineTarget<...> | undefined` and must be narrowed before sending. When no -parent declaration is present, callbacks do not expose a `parent` property. -`emit` never sends to the parent: it only publishes on the emitting machine's -`emissions` stream. - -Every handler also receives `self`, which can be targeted with `sendTo` when a -later mailbox turn is required. Use `raise` instead for same-macrostep work. -Both `self` and `parent` are minimal `Machine.MachineTarget` values. The -shared `Machine.MachineReferences` context keeps -their input protocols separate without exposing snapshot or lifecycle APIs. -Structural state values use distinct names: `containingState` is the immediate -valued state in the same statechart, while `ancestors` maps valued ancestor -paths. `parent` always means the owning machine target. - -### Choose the target by scope - -| Builder | Use when | Preserves | -| ---------------- | ---------------------------------------- | ------------------------------------------------- | -| `target.none()` | Handling without selecting a destination | The complete current configuration | -| `target.local` | Moving inside the nearest compound scope | Ancestors and unrelated parallel regions | -| `target.branch` | Moving elsewhere under the active root | Omitted active ancestors and parallel regions | -| `target.full` | Replacing or selecting a complete root | Nothing implicit for a newly selected root | -| `target.history` | Restoring a declared history node | The remembered configuration or its typed default | - -Every required transition handler selects a target from its inline `to` -builder. Return a bare selection such as `to.local.Idle()` when the selected -builder supports zero-argument construction; the machine applies the same -default construction as `target.from()`. This includes empty schemas and -schemas whose constructor fields are all optional or defaulted. TypeScript -rejects the bare form when state data or nested configuration is required. -Call `.resolve(...)` when construction depends on handler context or the -transition needs to enqueue commands. An absent handler ignores the trigger; `to.none` handles -it and retains queued commands, raised events, and emitted events without -selecting a destination. Concrete destinations stay narrowed inside their -resolver, and `to.branches({...})` gives the resolver only the declared named -`select` builders. Builders describe the -next logical configuration. Shared states exit and enter only when paths -change; call `.reenter()` before `.from(...)`, `.decoded(...)`, or `.resolve(...)` -when the source must restart. Default-constructible targets can finish at -`.reenter()`. With `to.none`, reentry -restarts the source while retaining its configuration. - -Topology-only definition instructions are values: `to.none`, declared -`.initial` and history selections, and `to.local.with`. Concrete state and -choice destinations remain calls such as `to.local.Running()`. Runtime named -branch builders remain callable, including `select.unchanged()`, because their -result carries the selected branch evidence. - -### Update an active scope value - -Use `to.local.update(...)` to replace the value owned by the nearest active -compound scope without rebuilding its active child. Use -`to.branch..update(...)` for a valued compound or parallel ancestor of the -handler source: +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))` +constructs the explicitly selected subtree. Parallel constructors require every +entered region. Required initializers and final output implementations remain +part of machine readiness checking. -```ts -const handlers = { - Increment: (to) => to.branch.root.session.update.from(({ current }) => ({ count: current.count + 1 })) -} -``` +Use `guard: context => boolean` to decline before construction or commands. +For a resolver that can decline, explicitly add `declinable: true`; only then may +it return `context.decline()`. `reenter: true` restarts the handler's source +where that operation supports reentry. A retained source-value update does not +restart its lifecycle. -The update keeps the exact active descendants, their values, history records, -completion outputs, and unrelated parallel regions. It runs no exit or entry -actions and does not restart state-owned work. Eventless stabilization still -runs, so an `always` transition can react to the new value. +### Select destinations and retain owners -The plain update method remains useful when topology does not change. It is -also a static selection for a named branch: +All references come from the same root descriptor supplied to `make`: -```ts -to.branches({ - changed: { target: to.local.update }, - unchanged: { target: to.none } -}).resolve(({ select, event }) => - event.changed - ? select.changed.from({ count: event.count }) - : select.unchanged() -) -``` - -### Change topology and a retained owner together +| Declaration | Meaning | +| ----------------------------------------------------- | ------------------------------------------------------------------------- | +| `{ target: targets.root.Checkout.Review, from: ... }` | Enter a declared destination with its value. | +| `{ initial: targets.root.Checkout, from: ... }` | 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, from: ... }` | Replace a retained active owner's value. | +| `{ update: targets.root, from: ... }` | Replace root data and retain active descendants. | +| `{ none: true }` | Accept an event without changing the configuration. | -When a transition enters another child and also replaces a valued ancestor -that stays active, declare both operations on the same target: +An atomic transition can enter a destination and update one retained owner: ```ts -const handlers = { - CreatePlan: (to) => - to.local.SavingPlan() - .updating(to.branch.Ready) - .from(({ current, event }) => ({ - target: { request: { _tag: "Create", input: event.input } }, - update: { ...current, notice: null } - })) +Save: { + target: targets.root.Checkout.Saving, + update: targets.root.Checkout, + from: ({ event, ancestors }) => ({ + target: { request: event.request }, + update: { ...ancestors.Checkout, revision: event.revision } + }) } ``` -`to.local.SavingPlan()` selects topology. `.updating(to.branch.Ready)` names -the retained valued owner and makes its replacement mandatory. `.from(...)` -returns `{ target, update }` with constructor inputs for both values; -`.decoded(...)` returns already decoded values for both. `current` is that -owner's decoded value from the pre-transition snapshot. Use `.resolve(...)` -when constructing explicit children, mixing construction methods, or queuing -commands; its `target` and `owner` builders construct the two values. - -The topology change and owner replacement apply atomically in one microstep. -The owner does not exit or reenter, its work is not restarted, and destination -entry actions observe the new owner value. Eventless stabilization follows. -Only one retained owner may be replaced by a combined target. A `full` target, -or any target that exits the selected owner, does not expose `.updating`. -Named branches support value-only updates; a combined update declares its -destination directly. - -For a schema-less destination, construction remains explicit: - -```ts -to.local.Idle() - .updating(to.branch.Ready) - .resolve(({ current, output, owner, target }) => - target.from().update( - owner.decoded( - new Ready({ - ...current, - day: output, - notice: "Plan changed." - }) - ) - ) - ) -``` - -Both values derive from the same pre-transition snapshot and are validated -before lifecycle actions run. Competing transitions that write the same owner -conflict; document order and hierarchy select one writer rather than applying -last-write-wins behavior. - -Use `.guard(predicate)` before construction to decline an update without -constructing values or queuing commands. It is available on standalone and -combined updates. A false guard allows ancestor fallback. A resolver may also -return `decline()` with `{ declinable: true }` for decisions during resolution. -Call `.reenter()` before construction on event or invocation transitions when -the handler source should exit and enter again. Reentry applies to that source, -not to the retained ancestor whose value changed. +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)`. +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 selector omits `update` for schema-less scopes, atomic and final states, -inactive branches, parallel sibling regions, and choice resolvers. Updating a -parallel sibling requires an event handled by that region. +The runtime transition API does not replace arbitrary complete root +configurations. `initialConfiguration` and history defaults retain complete +configuration construction for startup and restoration. -Use `declinable: true` when a resolver may decide that its transition is not -enabled. Only that resolver receives `decline()`, and its return type expands to -accept the opaque declined result: +### Protocols and ownership -```ts -const handlers = { - Submit: (to) => - to.local.Saving().resolve( - ({ event, target, decline }) => accepts(event) ? target.from({ draft: event.draft }) : decline(), - { declinable: true } - ) -} -``` - -Declining discards work enqueued by that resolver. Event and eventless dispatch -continues with the next eligible ancestor; if no candidate accepts, no -transition is selected. This differs from `target.none()`, which consumes the -trigger and prevents an ancestor from handling it. `transitionDefinitions` -reports each handler's `acceptance` as `"required"` or `"declinable"` while -preserving the exact declared target branches. Choices and initial routing must -remain total and cannot use declinable transitions. Completion and invocation -outcomes have no ancestor candidate: declining one ignores that lifecycle -occurrence and leaves the current configuration active. - -Use `Machine.can` when a caller needs to test a concrete event against a -snapshot. The direct and machine-specialized forms have the same semantics: +`Machine.events` defines public messages. `Machine.internalEvents` adds +machine-local raised events; `Machine.emittedEvents` defines notifications. +Their `FromSchemas` counterparts accept existing tagged schemas. Public and +internal tags must be disjoint. Deferred event constructors are validated when +the runtime receives the event. -```ts -const canSubmit = yield * Machine.can(machine, snapshot, Submit({ draft })) - -const canMachine = Machine.can(machine) -const canCancel = yield * canMachine(snapshot, Cancel()) -``` +Declare `parent: Machine.parent(events)` for a required owner protocol, or +`Machine.optionalParent(events)` for a machine that may run independently. +Handlers receive typed `self` and, when declared, `parent` references. Resolvers +use their second `enqueue` parameter to raise, emit, send, or stop work. +Planning stays synchronous: perform asynchronous work in an invocation. -`can` returns `true` when at least one required or non-declined handler accepts -the event. Targetless transitions count as accepted. Invalid event input fails -with `MachineSchemaDecodeError`; a valid unhandled event returns `false`. -Declinable resolvers run to decide acceptance, but collected commands, -emissions, and raised events are discarded. Required resolvers and transition -lifecycle do not run. +`Machine.prepare(machine)` exposes an `inspection` stream for the live local +machine tree; subscribe before running `prepared.start` to observe startup. Distributed +placement, transport, routing, and identity belong to the explicit Cluster +adapter. ## Statechart capabilities -`Machine.state` supports: - -- atomic states; -- compound states with one active child; -- parallel states with one active state in every region; -- final states and typed outputs; -- transient choice states; -- shallow and deep history states. - -Declare topology—including finality, output schemas, choices, and history—only -in `states`. Handlers implement behavior and output computation without -repeating structural metadata. Final children complete their parent, so -`onDone` belongs on that compound or parallel parent. +The same root supports compound and parallel states, eventless `always` +transitions, ordered named choices, final states, `onDone` completion, +initializers, shallow and deep history, and retained snapshots. The topology +is captured directly from declarations; visualization and branch coverage do +not execute user resolvers or depend on TypeScript source extraction. -Transition, entry, exit, choice, initial, and history callbacks are -synchronous. Conditions use ordinary TypeScript control flow. Callbacks may -select state and enqueue explicit `raise`, `emit`, `sendTo`, or `stop` commands; -arbitrary asynchronous Effects do not run inside planning. +A compound root completes with its direct completed workflow's output unless +that child handles completion first. Parallel completion waits for all required +regions. An arbitrary final descendant does not finish every ancestor. ## Effects, Streams, timers, and child machines -State-scoped work starts on entry and is interrupted on exit: - -```ts -machine.handle({ - states: { - Loading: { - invoke: (from) => - from.effect("save-document", () => saveDocument) - .onDone((to) => to.branch.Saved().resolve(({ output, target }) => target.from({ id: output.id }))) - .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: String(error) }))) - }, - Waiting: { - invoke: (from) => - from.timer("save-timeout", "3 seconds") - .onDone((to) => to.branch.Failed().resolve(({ target }) => target.from({ message: "Timed out" }))) - } - } -}) -``` - -The state-local `from` selector starts an `effect`, `stream`, `timer`, reusable -`logic`, or complete `child` statechart. The selected source determines which -lifecycle methods the chain requires and which methods are available. For -example, an Effect with non-`never` output and error channels must handle both; -the completed chain is the value returned by the callback: - -```ts -machine.handle({ - states: { - Loading: { - invoke: (from) => - from.effect("load-document", ({ state }) => loadDocument(state.documentId)) - .onDone((to) => to.branch.Ready().resolve(({ output, target }) => target.from({ document: output }))) - .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: error.message }))) - } - } -}) -``` - -A Stream source remains independent of the parent event protocol. Each element -is mapped by `onElement`, and the next element is not pulled until that parent -macrostep commits: +Register programs inside `make`, then invoke them by `src`: ```ts -machine.handle({ - states: { - Listening: { - invoke: (from) => - from.stream("channel", () => channelMessages) - .onElement((to) => - to.none.resolve(({ element }, enqueue) => { - enqueue.raise(Events.MessageReceived({ message: element })) - }) - ) - .onDone((to) => to.none) - .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ error }))) - } - } -}) -``` - -`to.none` is the targetless transition value. Return it directly to keep the -current configuration, or call `.resolve(...)` when the transition only needs -to enqueue commands. A block resolver may omit its return because it is -contextually typed to return `undefined`. - -Inside `.handle(...)`, `from` receives the owning machine's public input and -declared parent protocol contextually. Source and lifecycle callbacks can send -through `self` and `parent` while retaining the invoked Effect's output and -error inference: - -```ts -const machine = Machine.make({ - events: Commands, - internalEvents: InternalEvents, - parent: Machine.parent(ParentEvents) - // ... -}).handle({ - states: { - Saving: { - invoke: (from) => - from.effect("notify-parent", () => saveDocument) - .onDone((to) => - to.none.resolve(({ parent, self }, enqueue) => { - enqueue.sendTo(self, Commands.Save()) - enqueue.sendTo(parent, ParentEvents.ChildFinished({ id: "job-1" })) - }) - ) - .onFailure((to) => to.none) - } - } -}) -``` - -Return an array of completed chains to compose multiple state-owned activities. -The source computation itself, process logic, or `Machine.child(id, machine)` -descriptor can be named and reused; the invocation chain stays local so its -transitions retain the exact owning state and machine protocols. - -```ts -const refreshCache = Cache.refresh - -machine.handle({ - states: { - Active: { - invoke: (from) => [ - from.effect("refresh-cache", () => refreshCache).onDone((to) => to.none).onFailure((to) => to.none), - from.timer("expire-session", "5 minutes").onDone((to) => to.branch.Expired()) - ] - } - } -}) -``` - -`onDone` is required for a non-`never` output, and `onFailure` is required for a -non-`never` typed error. Streams additionally require `onElement` when their -element channel is non-`never` and always require `onDone`; logic and child -chains optionally expose `onSnapshot`. A handled method disappears from the -next builder step, so every reachable lifecycle channel is handled exactly -once. Defects, interruption, and source-construction failures terminate the -owning runtime. Effect sources are factories evaluated when their state is -entered. Use an Effect containing `Effect.sleep(...)` for generic work, while -`from.timer(...)` keeps timer intent explicit and makes static durations visible -through activity inspection. - -### Spawn dynamic child machines - -Use `from.child(...)` when a state owns a fixed child lifecycle. Use the -`children` context inside an invoked Effect when the machine process owns an -open set of children that must survive state changes: - -```ts -const Plant = Machine.childFamily(plantMachine) - -const central = Machine.make({ - events: Machine.eventsFromSchemas(ResourcesOffered, PlantBroken) - // ... +const Search = Machine.make({ + root: Root, + events: Events, + effects: { + load: (query: string) => searchItems(query), + refreshCache: Cache.refresh + }, + streams: { updates: changes }, + timers: { timeout: "5 seconds" }, + logic: { worker: workerLogic }, + children: { validation: ValidationChild } }).handle({ states: { - Commissioning: { - invoke: (from) => - from.effect("commission-wave", ({ children, state }) => - Effect.forEach( - state.plants, - (input) => children.spawn(Plant(input.id), { input }), - { discard: true } - )) - .onDone((to) => to.branch.Operating()) - .onFailure((to) => to.branch.CommissioningFailed()) + Loading: { + invoke: { + src: "load", + input: ({ state }) => state.query, + onDone: { target: targets.root.Ready, from: ({ output }) => ({ items: output }) }, + onFailure: { target: targets.root.Failed, from: ({ error }) => ({ message: error.message }) } + } } } }) ``` -`children.spawn` completes after initialization. The new child remains owned -by the machine process after the commissioning Effect completes or its state -exits. `children.sendTo` and `children.stop` address one active child from an -Effect; transition resolvers use `enqueue.sendTo` and `enqueue.stop` with the -same descriptor. Duplicate active ids fail with `ChildAlreadyExistsError` and -do not replace the existing child. Earlier successful spawns remain active if -a later spawn in the same wave fails. - -The child machine's declared `Machine.parent(...)` events must be accepted by -the owner. This is checked at each spawn call even though ids and cardinality -remain dynamic. `scope.spawn(child, { input })` provides the same descriptor -form for lower-level process logic, where the process event protocol is known. +Effects and Streams are lazy values: pass them directly when they need no +input. Use `Effect.suspend` or `Stream.suspend` when construction itself must be +deferred. An input-taking source has exactly one required parameter and the +invocation must supply a typed `input` mapper. A direct value forbids `input`. +Zero-argument and optional-argument factories are rejected to keep this choice +explicit. Source names are unique across all five registries. + +| Source | Required handlers | Optional handlers | +| -------------- | --------------------------------------------------------------------------------------------------- | ----------------- | +| Effect | `onDone` when success is not `never`; `onFailure` when error is not `never` | None | +| Stream | `onElement` when its element is not `never`; `onFailure` when error is not `never`; always `onDone` | None | +| Timer | `onDone` | None | +| Logic or child | Reachable `onDone` and `onFailure` | `onSnapshot` | + +A `void` success still requires `onDone`. Unreachable channels are rejected. +Startup errors, defects, and interruption follow the existing runtime failure +and cancellation boundaries; a typed `onFailure` handles the source's declared +runtime error channel. + +```ts +invoke: ; +;[ + { + src: "updates", + onElement: { + none: true, + resolve: ({ element }, enqueue) => { + enqueue.raise(Events.Changed({ value: element })) + } + }, + onDone: { none: true }, + onFailure: { none: true } + }, + { src: "timeout", onDone: { target: targets.root.Expired } }, + { src: "worker", id: "worker", address: WorkerAddress, onSnapshot: { none: true } }, + { src: "validation", input: ({ state }) => ({ query: state.query }), onDone: { none: true } } +] +``` + +Include each handler demanded by the concrete program's type. Use an explicit +`id` for simultaneous instances of one source; effect, stream, and timer IDs +default to `src`. Logic requires a lifecycle `id` and a typed `Machine.childAddress`. +A child uses its `Machine.child` descriptor's identity and protocol. All +invocations are owned by their state and cancelled when it exits. + +Effect services flow through the used sources and lifecycle effects into the +machine's requirements. Unused registrations add no requirements. Supply them +with ordinary `Effect.provide` / `Effect.provideService`, or an Atom runtime +backed by a Layer. Context services provide implementation substitution and +scoped resources; there is no separate machine-specific dependency container. + +### Dynamic children + +A registered `Machine.child` describes a fixed state-owned child. For an open +set of process-owned children, use `Machine.childFamily` with the `children` +capability supplied to a registered Effect through its input mapper. +`children.spawn` completes after child initialization; the child survives the +commissioning invocation and state. `children.sendTo` and `children.stop` +address it from Effects; resolvers use `enqueue.sendTo` and `enqueue.stop`. +Duplicate active IDs fail with `ChildAlreadyExistsError` and preserve the +existing child. Parent-protocol compatibility is checked at each spawn. ## Reactivity @@ -902,8 +481,8 @@ import { MachineTest } from "@typeonce/effect-machine/testing" const trace = yield* MachineTest.run(Counter, { events: [ - { _tag: "Start" }, - { _tag: "Increment" } + { _tag: "Increment", by: 1 }, + { _tag: "Increment", by: 2 } ] }) @@ -939,8 +518,8 @@ pnpm install --frozen-lockfile pnpm check ``` -Use `.guard(predicate)` to decline a transition before state construction, or -`.resolve(..., { declinable: true })` to use `decline()` during resolution. +Use `guard: predicate` to decline before construction, or `declinable: true` +when a resolver can return `decline()`. Pull requests that change `src/` or `package.json` need a changeset and the performance checks described in `AGENTS.md`. diff --git a/packages/effect-machine/docs/agent-guide.md b/packages/effect-machine/docs/agent-guide.md index 38ed3ce1..4898687f 100644 --- a/packages/effect-machine/docs/agent-guide.md +++ b/packages/effect-machine/docs/agent-guide.md @@ -35,6 +35,8 @@ export const CounterEvents = Machine.eventsFromSchemas( }) ) +const targets = Machine.targets(CounterStates) + export const CounterMachine = Machine.make({ id: "Counter", root: CounterStates, @@ -43,13 +45,13 @@ export const CounterMachine = Machine.make({ }).handle({ states: { Idle: { on: { - Start: (to) => to.branch.Running().resolve(({ target }) => target.from({ count: 0 })) + Start: { target: targets.root.Running, from: () => ({ count: 0 }) } } }, Running: { on: { - Increment: (to) => to.branch.Running().resolve(({ state, target }) => target.from({ count: state.count + 1 })), - Stop: (to) => to.branch.Idle() + Increment: { update: targets.root.Running, from: ({ state }) => ({ count: state.count + 1 }) }, + Stop: { target: targets.root.Idle } } } } }) @@ -171,16 +173,16 @@ const DocumentEvents = Machine.eventsFromSchemas( }) ) +const documentTargets = Machine.targets(DocumentStates) const DocumentMachine = Machine.make({ root: DocumentStates, - events: DocumentEvents, - initial: (to) => to.Closed() + events: DocumentEvents }).handle({ states: { Closed: {}, Open: { on: { // All Open children close the document in the same way. - Close: (to) => to.branch.Closed() + Close: { target: documentTargets.root.Closed } }, states: { Editing: {}, @@ -209,20 +211,20 @@ export const CheckoutEvents = Machine.eventsFromSchemas( }) ) +const checkoutTargets = Machine.targets(CheckoutStates) const CheckoutMachine = Machine.make({ root: CheckoutStates, - events: CheckoutEvents, - initial: (to) => to.Editing() + events: CheckoutEvents }).handle({ states: { Editing: { on: { - Submit: (to) => to.branch.Submitting() + Submit: { target: checkoutTargets.root.Submitting } } }, Submitting: { on: { // Cancel has meaning while work is in progress. - Cancel: (to) => to.branch.Editing() + Cancel: { target: checkoutTargets.root.Editing } } }, Complete: {} @@ -293,18 +295,21 @@ const LoadStates = Machine.state({ initial: "Idle", states: { Failed: LoadState.cases.Failed } }) +const loadTargets = Machine.targets(LoadStates) const LoadMachine = Machine.make({ root: LoadStates, + effects: { loadDocument }, events: Machine.eventsFromSchemas(), initialConfiguration: root => root.resolve(({ target }) => target.from(to => to.Idle.from())) }).handle({ states: { Idle: {}, Loading: { - invoke: (from) => - from - .effect("load-document", ({ state }) => loadDocument(state.documentId)) - .onDone((to) => to.branch.Ready().resolve(({ output, target }) => target.from({ content: output }))) - .onFailure((to) => to.branch.Failed().resolve(({ error, target }) => target.from({ message: String(error) }))) + invoke: { + src: "loadDocument", + input: ({ state }) => state.documentId, + onDone: { target: loadTargets.root.Ready, from: ({ output }) => ({ content: output }) }, + onFailure: { target: loadTargets.root.Failed, from: ({ error }) => ({ message: String(error) }) } + } }, Ready: {}, Failed: {} @@ -323,10 +328,16 @@ logic or child process that can be active at the same time a unique runtime address as well: ```ts +// Register timers: { loadTimeout: "10 seconds" } in make. Loading: { - invoke: (from) => [ - from.effect("load-document", loadDocument), - from.timer("load-timeout", "10 seconds") + invoke: [ + { + src: "loadDocument", + input: ({ state }) => state.documentId, + onDone: { target: loadTargets.root.Ready, from: ({ output }) => ({ content: output }) }, + onFailure: { target: loadTargets.root.Failed, from: ({ error }) => ({ message: String(error) }) } + }, + { src: "loadTimeout", onDone: { target: loadTargets.root.Idle } } ] } ``` @@ -339,27 +350,13 @@ reuse its identity. ### Choose state-owned or process-owned children -Use `from.child(...)` when the child belongs to one state and must stop when -that state exits. Use a child family and `children.spawn(...)` when runtime -events determine the ids or cardinality and the children must survive owner -state changes: - -```ts -const Worker = Machine.childFamily(workerMachine) - -Commissioning: { - invoke: (from) => - from.effect("start-workers", ({ children, state }) => - Effect.forEach( - state.workers, - (input) => children.spawn(Worker(input.id), { input }), - { discard: true } - ) - ) - .onDone((to) => to.branch.Running()) - .onFailure((to) => to.branch.Failed()) -} -``` +Register a descriptor in `make({ children: { worker: Worker } })` and use +`invoke: { src: "worker", ...outcomes }` when a child belongs to one state and +must stop when that state exits. Use a child family and `children.spawn(...)` +inside a registered Effect when runtime events determine ids or cardinality +and the children must survive owner state changes. See the +[dynamic children example](../README.md#dynamic-children) for the complete +input mapping and service requirements. The Effect owns the startup attempt. The machine process owns every child that starts successfully. Leaving `Commissioning` does not stop those children. @@ -380,24 +377,25 @@ const ReviewEvents = Machine.eventsFromSchemas( }) ) +const reviewTargets = Machine.targets(ReviewStates) const ReviewMachine = Machine.make({ root: ReviewStates, events: ReviewEvents, - initial: (to) => to.Pending() + branches: { + evaluate: { + accepted: { target: reviewTargets.root.Accepted }, + rejected: { target: reviewTargets.root.Rejected } + } + } }).handle({ states: { Pending: { on: { - Evaluate: (to) => - to - .branches({ - accepted: { target: to.branch.Accepted() }, - rejected: { target: to.branch.Rejected() } - }) - .resolve(({ event, select }) => - event.score >= 80 - ? select.accepted.from() - : select.rejected.from() - ) + Evaluate: { + branches: "evaluate", + resolve: ({ event, select }) => event.score >= 80 + ? select.accepted.from() + : select.rejected.from() + } } }, Accepted: {}, @@ -410,50 +408,27 @@ Do not read the clock, generate randomness, call a service, or await work while choosing a transition. Receive such values in an event or produce them through state-owned work first. -When only an active compound or parallel state's value changes, use its static -update selection instead of reconstructing its active descendants: +When only an active state's value changes, use `update` to preserve its active +descendants and running work: ```ts -Changed: (to) => - to.local.update(({ current, owner }) => - owner.from({ revision: current.revision + 1 }) - ) -``` - -`to.local.update` addresses the nearest valued compound scope. -`to.branch..update` addresses a valued compound or parallel ancestor of -the handler source. Both preserve the complete active descendant -configuration. Neither runs lifecycle actions or restarts state-owned work by -default. Use an event handled inside a parallel sibling when that sibling owns -the value that must change. - -When topology changes and one valued ancestor remains active but needs a new -value, declare the owner on the destination: - -```ts -CreatePlan: (to) => - to.local.SavingPlan() - .updating(to.branch.Ready) - .resolve(({ current, event, owner, target }) => - target.from({ request: event.input }).update( - owner.decoded(new Ready({ ...current, notice: null })) - ) - ) +Changed: { + update: targets.root.Document, + from: ({ ancestors }) => ({ + ...ancestors.Document, + revision: ancestors.Document.revision + 1 + }) +} ``` -`.updating(...)` accepts the retained state selector itself. It makes the -owner replacement mandatory at compile time: the resolver must finish a -destination construction with `.update(...)`. `current` is the owner's -decoded pre-transition value. `target` constructs the destination and `owner` -constructs the complete replacement value. - -Both instructions are validated and applied atomically. The retained owner is -not reentered, destination entry sees its new value, and eventless -stabilization runs afterward. Only local and branch targets that retain the -owner expose `.updating`; full targets do not. Combined targets support one -owner. Keep separate domain changes explicit by constructing the complete -owner value instead of relying on a partial merge helper. Combined updates use -a direct resolver; named branches support value-only updates. +Choose a valued source or retained ancestor explicitly. To change a parallel +sibling, send an event handled by that sibling. Combine a destination and one +retained owner update with `{ target, update, from }`; the callback returns +`{ target: destinationInput, update: completeOwnerInput }`. Both values are +validated atomically, and destination entry sees the new owner value. A named +branch can declare the same pair when a resolver needs commands or nested +construction. Its constructor requires `.update.from(...)` or +`.update.decoded(...)` before the selection can be returned. ## Test paths and invariants diff --git a/packages/effect-machine/docs/machine-review.md b/packages/effect-machine/docs/machine-review.md index 931d48b0..597519d6 100644 --- a/packages/effect-machine/docs/machine-review.md +++ b/packages/effect-machine/docs/machine-review.md @@ -30,40 +30,18 @@ Use this split when deciding where code belongs: If a component coordinates a workflow, an atom performs business work, or a provider has no ownership purpose, inspect that code more closely. -## Remove identity resolvers +## Keep ordinary transitions inline -A target that supports default construction does not need a resolver whose only -job is returning `target.from()`. +Use `{ target: targets.root.Running }` for a state with default construction. +Use `{ target: targets.root.Running, from: ({ event }) => ({ count: event.count }) }` +when the destination needs data. The checker rejects missing required fields. +Use `decoded` for values already decoded by their schema. -```ts -// Redundant -const handlers = { - Start: (to) => - to.branch.Running().resolve(({ target }) => target.from()) -} - -// Preferred -const handlers = { - Start: (to) => to.branch.Running() -} -``` - -This applies to schema-less states and schemas whose constructor fields are all -optional or defaulted. The type checker rejects the shorter form when the -target needs data. - -Keep `.resolve(...)` when it uses handler context, constructs state data, -updates a retained owner, chooses a branch, declines a transition, or enqueues -commands. For resolver-free reentry, use `.reenter()`: - -```ts -const handlers = { - Refresh: (to) => to.local.Ready().reenter() -} -``` - -Review check: search for `.resolve(...)` callbacks that only return an empty -`target.from()` and remove the callback. +Reserve named branch groups and `resolve` for conditional outcomes, commands, +or nested construction. The group lives in `make`; its resolver selects only +constructors derived from those declarations. Avoid an extra branch group for +an ordinary transition. Use `reenter: true` when the source must restart. +An empty targetless resolver simplifies to `{ none: true }`. ## Choose React ownership or keyed family lookup @@ -176,29 +154,21 @@ Model `Submit` as the component-facing event. Let a machine state own the work and its lifetime: ```ts +// In make: effects: { submitOrder } +// targets is Machine.targets(OrderStates). machine.handle({ states: { Editing: { on: { - Submit: (to) => - to.branch.Submitting().resolve(({ event, target }) => - target.from({ order: event.order }) - ) + Submit: { target: targets.root.Submitting, from: ({ event }) => ({ order: event.order }) } } }, Submitting: { - invoke: (from) => - from - .effect("submit-order", ({ state }) => submitOrder(state.order)) - .onDone((to) => - to.branch.Complete().resolve(({ output, target }) => - target.from({ order: output }) - ) - ) - .onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.from({ message: String(error) }) - ) - ) + invoke: { + src: "submitOrder", + input: ({ state }) => state.order, + onDone: { target: targets.root.Complete, from: ({ output }) => ({ order: output }) }, + onFailure: { target: targets.root.Failed, from: ({ error }) => ({ message: String(error) }) } + } } } }) ``` diff --git a/packages/effect-machine/docs/root-api.md b/packages/effect-machine/docs/root-api.md index 00296ff0..73c5f5a3 100644 --- a/packages/effect-machine/docs/root-api.md +++ b/packages/effect-machine/docs/root-api.md @@ -14,10 +14,11 @@ const Root = Machine.state({ initial: "Closed", states: { Closed: {}, Open: {} } }) +const targets = Machine.targets(Root) const Door = Machine.make({ root: Root, events: Events }).handle({ states: { - Closed: { on: { Open: (to) => to.local.Open() } }, - Open: { on: { Close: (to) => to.local.Closed() } } + Closed: { on: { Open: { target: targets.root.Open } } }, + Open: { on: { Close: { target: targets.root.Closed } } } } }) ``` @@ -37,6 +38,7 @@ const Root = Machine.state({ } }) const Events = Machine.events({ Rename: { title: Schema.String }, Save: {} }) +const targets = Machine.targets(Root) const Editor = Machine.make({ root: Root, events: Events, @@ -44,10 +46,10 @@ const Editor = Machine.make({ }).handle({ initialize: ({ builder, state }) => builder.from({ draft: state.title }), on: { - Rename: (to) => to.self.update.from(({ event }) => ({ title: event.title })) + Rename: { update: targets.root, from: ({ event }) => ({ title: event.title }) } }, states: { - Editing: { on: { Save: (to) => to.local.Saving() } } + Editing: { on: { Save: { target: targets.root.Saving } } } } }) ``` @@ -72,14 +74,15 @@ The complete snapshot always includes the root: ``` Root handlers remain active across child transitions. Child handlers receive -their own state, typed ancestors, and the complete snapshot. `to.root.update` -addresses root data from a child. `to.self.update` addresses the handler owner. +their own state, typed ancestors, `root`, and the complete snapshot. +`update: targets.root` addresses root data from a child. An explicit descendant +reference addresses a valued handler owner or retained ancestor. Both replace the entire value; omitted fields are not silently retained. Updates preserve the active topology and do not restart scoped work. Simultaneous transitions retain conflict checking. Two parallel handlers cannot -silently overwrite the same owner. Use `.updating(owner)` to combine a selected -destination and a retained owner's value in one atomic transition. +silently overwrite the same owner. Use `{ target, update }` to combine a selected destination and a retained +owner's value in one atomic transition. ## Schemas and reusable descriptors @@ -139,75 +142,72 @@ a validated snapshot, including its completion and history metadata. ## Construction, guards, and branches -A selected target supports a direct callback: +Ordinary transitions declare their destination inline. `from` constructs schema +make input; `decoded` returns an already decoded value. Omit construction only +when the selected state can be constructed without arguments. ```ts -Save: (to) => to.local.Saving().from(({ event }) => ({ requestId: event.requestId })) +Save: { + target: targets.root.Saving, + guard: ({ state }) => state.draft.length > 0, + from: ({ state }) => ({ requestId: state.draft }) +} ``` -Use `.decoded(({ state }) => state)` for already decoded schema values. Use -`.resolve((context, enqueue) => ...)` when constructing child configurations or -queuing commands; its `context.target.from(...)` and `.decoded(...)` take values -and explicit child builders. +A false guard declines the handler and allows ancestor fallback. `{ none: true }` +accepts an event without changing topology. Initial entry and total choices +cannot decline. `reenter: true` explicitly restarts the handler source; a value +update alone retains its lifecycle and cannot request reentry. -A guard runs before construction and commands: +Use a branch group in `make` for conditional selection, commands, or explicit +nested construction. The resolver receives constructors derived from that +exact declaration, so its result cannot introduce another destination. ```ts -Save: (to) => to.local.Saving() - .guard(({ state }) => state.draft.length > 0) - .from(({ state }) => ({ requestId: state.draft })) -``` - -A false guard declines this handler, allowing ancestor fallback. Guards are not -available for mandatory initial resolution or a total choice. `.none` accepts -an event without changing topology; declining and accepting have different -semantics. Named branches keep `{ target, title? }`: `target` identifies the -checked destination and `title` supplies optional presentation metadata. - -Guards also apply to standalone owner updates and combined transitions: +// In make: +branches: { + choose: { + saving: { target: targets.root.Saving, title: "Save changes" }, + idle: { target: targets.root.Idle } + } +} -```ts -Increment: (to) => to.self.update - .guard(({ current }) => current.count < 10) - .from(({ current }) => ({ ...current, count: current.count + 1 })) - -Save: (to) => to.local.Saving().updating(to.root) - .guard(({ current }) => current.draft.length > 0) - .from(({ current }) => ({ - target: { requestId: current.draft }, - update: { ...current, attempts: current.attempts + 1 } - })) +// In handle: +Choose: { + branches: "choose", + resolve: ({ event, select }) => event.save + ? select.saving.from({ requestId: event.requestId }) + : select.idle.from() +} ``` -Combined `.from` returns constructor inputs for both the destination and the -complete owner replacement. `.decoded` returns their decoded values instead. -For a destination with no construction arguments, use `target: undefined`. -Both values are validated before applying either change; destination entry -observes the updated owner. Use `.resolve` for mixed construction methods, -explicit child configurations, or commands. +Declare `declinable: true` if a resolver may return `decline()`. Guards and +resolvers must remain synchronous and deterministic. -Reentry is a modifier before construction: +An owner update replaces its entire value. A combined transition constructs +both values atomically: ```ts -Retry: (to) => to.local.Saving().reenter() - .from(({ event }) => ({ requestId: event.requestId })) - -Refresh: (to) => to.none.reenter() - -Choose: (to) => to.branches({ - saving: { target: to.local.Saving() }, - idle: { target: to.local.Idle() } -}).reenter().resolve(({ state, select }) => - state.retry ? select.saving.from({ requestId: state.requestId }) : select.idle.from() -) +Save: { + target: targets.root.Saving, + update: targets.root, + from: ({ root, event }) => ({ + target: { requestId: event.requestId }, + update: { ...root, attempts: root.attempts + 1 } + }) +} ``` -`.reenter()` restarts the handler source. It composes with `.updating`, `.guard`, -`.from`, `.decoded`, and `.resolve` wherever reentry is supported. Apply it to -the whole named-branches builder, whose individual targets describe topology. -Migrate `.resolve(callback, { reenter: true })` to `.reenter().resolve(callback)`; -remove `{ reenter: false }`. `to.self.update` retains the source lifecycle and -does not expose `.reenter()`. +Destination entry sees the updated owner. Use a named branch with the same +`{ target, update }` declaration for nested construction or mixed methods: +`select.saved.from(payload).update.decoded(ownerValue)`. + +All destination references are paths derived by `Machine.targets(Root)`. +`target` selects a descendant; `initial` enters a compound or parallel subtree +using its declared initialization; `history` restores a history reference. +`update: targets.root` changes root data while preserving active children. +There is no transition operation that replaces an arbitrary full root +configuration. Use explicit branches to keep every possible destination visible. ## Completion and history diff --git a/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs b/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs index c6a5dc49..ff954f7a 100644 --- a/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs +++ b/packages/effect-machine/perf/runtime/effect-machine-compatibility.mjs @@ -8,6 +8,9 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { // The legacy process constructor takes `(initial, transition)`. The static // definition constructor is deliberately unary and returns its config. + const hasObjectTransitions = typeof Machine.targets === "function" + const transitionDefinition = Symbol("benchmark transition") + const invocationDefinition = Symbol("benchmark invocation") const hasRoot = typeof Machine.eventsFromSchemas === "function" const snapshot = (value) => hasRoot ? value.state : value const hasStaticTransitions = typeof Machine.transition === "function" && Machine.transition.length === 1 @@ -59,12 +62,53 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { return [key, selected] })) const initialKey = selectInstruction(definition.target(selectors)) - const machine = Machine.make({ ...rest, root, initialConfiguration: (root) => root.resolve((context) => + const initialConfiguration = (root) => root.resolve((context) => context.target.from((tree) => { const target = tree[initialKey] return definition.resolve === undefined ? target.from() : definition.resolve({ ...context, target }) - })) }) - return { handle: (states) => machine.handle({ states }) } + })) + if (!hasObjectTransitions) { + const machine = Machine.make({ ...rest, root, initialConfiguration }) + return { handle: (states) => machine.handle({ states }) } + } + const targets = Machine.targets(root) + const referenceSelectors = (refs) => Object.fromEntries(Object.entries(refs).map(([key, ref]) => { + const selected = () => ref + Object.assign(selected, referenceSelectors(ref)) + return [key, selected] + })) + const selectorsForRoot = referenceSelectors(targets.root) + const children = {} + const transition = (value, siblings) => { + const definition = value?.[transitionDefinition] + if (definition === undefined) return value + const target = selectInstruction(definition.target({ full: selectorsForRoot, branch: selectorsForRoot, local: siblings })) + return { + target, + ...(definition.resolve === undefined ? {} : { from: (context) => definition.resolve({ ...context, target: { from: (value) => value } }) }), + ...(definition.reenter === true ? { reenter: true } : {}) + } + } + const walk = (nodes, refs) => Object.fromEntries(Object.entries(nodes).map(([key, node]) => { + const siblings = referenceSelectors(refs) + const result = { ...node } + if (node.on !== undefined) result.on = Object.fromEntries(Object.entries(node.on).map(([event, value]) => [event, transition(value, siblings)])) + if (node.states !== undefined) result.states = walk(node.states, refs[key]) + if (node.invoke !== undefined) { + const config = node.invoke[invocationDefinition] + if (config === undefined) throw new Error("Unsupported benchmark invocation") + const src = `child${Object.keys(children).length}` + children[src] = config.child + const { child, ...options } = config + result.invoke = { src, ...options } + } + return [key, result] + })) + return { handle: (states) => { + const handlers = walk(states, targets.root) + return Machine.make({ ...rest, root, initialConfiguration, children }).handle({ states: handlers }) + } } + }, states: (definitions) => hasRoot ? { states: Machine.state({ initial: Object.keys(definitions)[0], states: definitions }) } : typeof Machine.states === "function" ? Machine.states(definitions) : Machine.defineStates(definitions), @@ -76,17 +120,17 @@ export const makeEffectMachineBenchmarkApi = (Machine) => { : hasStaticTransitions || hasFluentTransitions ? objectInitial(definition) : legacy, - transition: (definition, legacy) => hasStaticTransitions + transition: (definition, legacy) => hasObjectTransitions ? { [transitionDefinition]: definition } : hasStaticTransitions ? Machine.transition(objectTransition(definition)) : hasFluentTransitions ? fluentTransition(definition) : legacy, - targetless: hasValueSelectors + targetless: hasObjectTransitions ? { none: true } : hasValueSelectors ? (to) => to.none : hasStaticTransitions || hasFluentTransitions ? { target: Machine.targetless } : targetless, - invokeChild: hasFluentInvocations + invokeChild: hasObjectTransitions ? (config) => ({ [invocationDefinition]: config }) : hasFluentInvocations ? (config) => (from) => { let invoked = config.input === undefined ? from.child(config.child) diff --git a/packages/effect-machine/perf/types/adapter-readiness-control.ts b/packages/effect-machine/perf/types/adapter-readiness-control.ts index 5aca004a..ced9d686 100644 --- a/packages/effect-machine/perf/types/adapter-readiness-control.ts +++ b/packages/effect-machine/perf/types/adapter-readiness-control.ts @@ -32,6 +32,7 @@ export const snapshot = { state: { path: "Ready" as const, value: Ready.make({}) } } +const targets1 = Machine.targets(States) export const machine = Machine.make({ id: "perf-readiness", root: States, @@ -49,7 +50,7 @@ export const machine = Machine.make({ states: { Idle: {}, Route: { - choice: (to) => to.branch.Ready().resolve(({ target }) => target.from(Ready.make({}))) + choice: { target: targets1.root.Ready, from: () => (Ready.make({})) } } } }, diff --git a/packages/effect-machine/perf/types/composition.ts b/packages/effect-machine/perf/types/composition.ts index f01a1064..59cb31ef 100644 --- a/packages/effect-machine/perf/types/composition.ts +++ b/packages/effect-machine/perf/types/composition.ts @@ -1,17 +1,6 @@ import { Context, Data, Effect } from "effect" import { Machine } from "../../dist/index.js" -import { - App, - Editing, - Editor, - EditorDone, - machine, - States, - Sync, - SyncDone, - SyncIdle, - Workspace -} from "./composition-control.js" +import { App, Editing, Editor, EditorDone, States, Sync, SyncDone, SyncIdle, Workspace } from "./composition-control.js" type Equal = (() => Type extends Left ? 1 : 2) extends (() => Type extends Right ? 1 : 2) ? true : @@ -24,6 +13,27 @@ class CompositionService extends Context.Service()( ) {} class CompositionFailure extends Data.TaggedError("CompositionFailure")<{}> {} +const targets = Machine.targets(States) +const machine = Machine.make({ + branches: { enter: { app: { target: targets.root.App } } }, + + root: States, + events: Machine.eventsFromSchemas(), + initialConfiguration: (root) => + root.resolve(({ target }) => + target.from((to) => + to.App.from(App.make({}), (app) => + app.Workspace.from( + Workspace.make({}), + (workspace) => + workspace + .Editor.from(Editor.make({}), (editor) => editor.Editing.from(Editing.make({}))) + .Sync.from(Sync.make({}), (sync) => sync.Idle.from(SyncIdle.make({}))) + )) + ) + ) +}) + const handled = machine.handle({ states: { App: { @@ -70,9 +80,10 @@ const handled = machine.handle({ } }, Route: { - choice: (to) => - to.branch.App().resolve(({ target }) => - target.from(App.make({}), (app) => + choice: { + branches: "enter", + resolve: ({ select }) => + select.app.from(App.make({}), (app) => app.Workspace.from( Workspace.make({}), (workspace) => @@ -80,7 +91,7 @@ const handled = machine.handle({ .Editor.from(Editor.make({}), (editor) => editor.Editing.from(Editing.make({}))) .Sync.from(Sync.make({}), (sync) => sync.Idle.from(SyncIdle.make({}))) )) - ) + } } } } diff --git a/packages/effect-machine/perf/types/definition-variants.ts b/packages/effect-machine/perf/types/definition-variants.ts index 996810a8..75f5ba75 100644 --- a/packages/effect-machine/perf/types/definition-variants.ts +++ b/packages/effect-machine/perf/types/definition-variants.ts @@ -1,5 +1,5 @@ import { Machine } from "../../dist/index.js" -import { Done, Flow, Idle, machine, Running } from "./definition-variants-control.js" +import { Done, Flow, Idle, machine, Running, States } from "./definition-variants-control.js" type Equal = (() => Type extends Left ? 1 : 2) extends (() => Type extends Right ? 1 : 2) ? true : @@ -7,6 +7,7 @@ type Equal = (() => Type extends Left ? 1 : 2) extends ( = Value type IsAny = 0 extends 1 & Value ? true : false +const targets = Machine.targets(States) const complete = machine.handle({ states: { Flow: { @@ -18,17 +19,16 @@ const complete = machine.handle({ }, states: { Route: { - choice: (to) => to.local.Idle().resolve(({ target }) => target.from(Idle.make({}))) + choice: { target: targets.root.Flow.Idle } }, Idle: { on: { - Start: (to) => to.local.Running().resolve(({ target }) => target.from(Running.make({}))) + Start: { target: targets.root.Flow.Running } } }, Running: { on: { - Finish: (to) => - to.local.Done().resolve(({ event, target }) => target.from(Done.make({ value: event.value }))) + Finish: { target: targets.root.Flow.Done, from: ({ event }) => ({ value: event.value }) } } }, Done: { @@ -45,7 +45,7 @@ const idleOnly = machine.handle({ states: { Idle: { on: { - Start: (to) => to.local.Running().resolve(({ target }) => target.from(Running.make({}))) + Start: { target: targets.root.Flow.Running } } } } @@ -59,8 +59,7 @@ const runningOnly = machine.handle({ states: { Running: { on: { - Finish: (to) => - to.local.Done().resolve(({ event, target }) => target.from(Done.make({ value: event.value }))) + Finish: { target: targets.root.Flow.Done, from: ({ event }) => ({ value: event.value }) } } } } diff --git a/packages/effect-machine/perf/types/dynamic-invoke.ts b/packages/effect-machine/perf/types/dynamic-invoke.ts index 077176ea..8ecc5355 100644 --- a/packages/effect-machine/perf/types/dynamic-invoke.ts +++ b/packages/effect-machine/perf/types/dynamic-invoke.ts @@ -1,28 +1,43 @@ import { Machine } from "../../dist/index.js" -import { LoadError, loadUser, machine } from "./dynamic-invoke-control.js" +import { LoadError, Loading, loadUser, States } from "./dynamic-invoke-control.js" interface User { readonly id: string readonly name: string } +const machine = Machine.make({ + effects: { "load-user": loadUser }, + + root: States, + events: Machine.eventsFromSchemas(), + initialConfiguration: (root) => + root.resolve(({ target }) => target.from((to) => to.Loading.from(Loading.make({ userId: "user-1" })))) +}) + const invoked = machine.handle({ states: { Loading: { - invoke: (from) => - from.effect("load-user", ({ state }) => loadUser(state.userId)).onDone((to) => - to.none.resolve(({ output }) => { + invoke: { + src: "load-user", + input: ({ state }) => state.userId, + onDone: { + none: true, + resolve: ({ output }) => { const user: User = output void user return undefined - }) - ).onFailure((to) => - to.none.resolve(({ error }) => { + } + }, + onFailure: { + none: true, + resolve: ({ error }) => { const loadError: LoadError = error void loadError return undefined - }) - ) + } + } + } } } }) diff --git a/packages/effect-machine/perf/types/exact-channels.ts b/packages/effect-machine/perf/types/exact-channels.ts index 8ff905e0..62c26f74 100644 --- a/packages/effect-machine/perf/types/exact-channels.ts +++ b/packages/effect-machine/perf/types/exact-channels.ts @@ -1,6 +1,6 @@ import { Effect } from "effect" import { Machine } from "../../dist/index.js" -import { Done, Loaded, machine, Notice, Start, States } from "./exact-channels-control.js" +import { Done, Idle, Input, Loaded, Notice, Start, States } from "./exact-channels-control.js" type Equal = (() => Type extends Left ? 1 : 2) extends (() => Type extends Right ? 1 : 2) ? true : @@ -8,17 +8,32 @@ type Equal = (() => Type extends Left ? 1 : 2) extends ( = Value type IsAny = 0 extends 1 & Value ? true : false +const targets = Machine.targets(States) +const machine = Machine.make({ + branches: { finish: { done: { target: targets.root.Done } } }, + + root: States, + events: Machine.eventsFromSchemas(Start), + internalEvents: Machine.internalEventsFromSchemas(Loaded), + emittedEvents: Machine.emittedEventsFromSchemas(Notice), + input: Input, + initialConfiguration: (root) => + root.resolve(({ input, target }) => target.from((to) => to.Idle.from(Idle.make({ value: input.seed })))) +}) + const complete = machine.handle({ states: { Idle: { entry: () => {}, on: { - Start: (to) => - to.branch.Done().resolve(({ event, target }, enqueue) => { + Start: { + branches: "finish", + resolve: ({ event, select }, enqueue) => { enqueue.emit(Notice.make({ value: event.value })) - return target.from(Done.make({ value: event.value })) - }), - Loaded: (to) => to.branch.Done().resolve(({ event, target }) => target.from(Done.make({ value: event.value }))) + return select.done.from(Done.make({ value: event.value })) + } + }, + Loaded: { target: targets.root.Done, from: ({ event }) => ({ value: event.value }) } } }, Done: { diff --git a/packages/effect-machine/perf/types/handle.ts b/packages/effect-machine/perf/types/handle.ts index f01e08bb..7a75f14b 100644 --- a/packages/effect-machine/perf/types/handle.ts +++ b/packages/effect-machine/perf/types/handle.ts @@ -14,20 +14,17 @@ const Event = Schema.TaggedUnion({ const States = Machine.state({ initial: "Idle", states: State.cases }) -const machine = Machine.make({ - root: States, - events: Machine.eventsFromSchemas(Event) -}).handle({ +const targets1 = Machine.targets(States) +const machine = Machine.make({ root: States, events: Machine.eventsFromSchemas(Event) }).handle({ states: { Idle: { on: { - Start: (to) => to.branch.Running().resolve(({ target }) => target.from(State.cases.Running.make({}))) + Start: { target: targets1.root.Running, from: () => (State.cases.Running.make({})) } } }, Running: { on: { - Finish: (to) => - to.branch.Done().resolve(({ event, target }) => target.from(State.cases.Done.make({ value: event.value }))) + Finish: { target: targets1.root.Done, from: ({ event }) => (State.cases.Done.make({ value: event.value })) } } }, Done: {} diff --git a/packages/effect-machine/perf/types/named-branches.ts b/packages/effect-machine/perf/types/named-branches.ts index a1a32970..98801a6f 100644 --- a/packages/effect-machine/perf/types/named-branches.ts +++ b/packages/effect-machine/perf/types/named-branches.ts @@ -1,24 +1,37 @@ import { Machine } from "../../dist/index.js" -import { machine, State } from "./named-branches-control.js" +import { Route, State, States } from "./named-branches-control.js" + +const targets = Machine.targets(States) +const machine = Machine.make({ + branches: { + route: { + length1: { target: targets.root.Text }, + length2: { target: targets.root.Count }, + length3: { target: targets.root.Text }, + length4: { target: targets.root.Count }, + length5: { target: targets.root.Text }, + length6: { none: true }, + length7: { target: targets.root.Count }, + length8: { target: targets.root.Text }, + length9: { target: targets.root.Count }, + length10: { target: targets.root.Idle }, + unchanged: { none: true } + } + }, + + root: States, + events: Machine.eventsFromSchemas(Route), + initialConfiguration: (root) => + root.resolve(({ target }) => target.from((to) => to.Idle.from(State.cases.Idle.make({})))) +}) const handled = machine.handle({ states: { Idle: { on: { - Route: (to) => - to.branches({ - length1: { target: to.branch.Text() }, - length2: { target: to.branch.Count() }, - length3: { target: to.branch.Text() }, - length4: { target: to.branch.Count() }, - length5: { target: to.branch.Text() }, - length6: { target: to.none }, - length7: { target: to.branch.Count() }, - length8: { target: to.branch.Text() }, - length9: { target: to.branch.Count() }, - length10: { target: to.branch.Idle() }, - unchanged: { target: to.none } - }).resolve(({ event, select }) => { + Route: { + branches: "route", + resolve: ({ event, select }) => { const value = event.value switch (value.length) { case 1: @@ -44,7 +57,8 @@ const handled = machine.handle({ default: return select.unchanged() } - }) + } + } } }, Text: {}, diff --git a/packages/effect-machine/perf/types/transition-construction-control.ts b/packages/effect-machine/perf/types/transition-construction-control.ts index 51ca12e5..e27f4449 100644 --- a/packages/effect-machine/perf/types/transition-construction-control.ts +++ b/packages/effect-machine/perf/types/transition-construction-control.ts @@ -4,12 +4,13 @@ import { Machine } from "../../dist/index.js" export class Root extends Schema.TaggedClass("Root")("Root", { count: Schema.Number }) {} export class Saved extends Schema.TaggedClass("Saved")("Saved", { text: Schema.String }) {} +export const States = Machine.state({ + schema: Root, + initial: "Idle", + states: { Idle: {}, Saved: { schema: Saved } } +}) export const machine = Machine.make({ - root: Machine.state({ - schema: Root, - initial: "Idle", - states: { Idle: {}, Saved: { schema: Saved } } - }), + root: States, events: Machine.events({ Save: { text: Schema.String }, Retry: {}, Reset: {} }), initial: (root) => root.from(() => ({ count: 0 })) }) diff --git a/packages/effect-machine/perf/types/transition-construction.ts b/packages/effect-machine/perf/types/transition-construction.ts index 470bbb82..0c2c57e6 100644 --- a/packages/effect-machine/perf/types/transition-construction.ts +++ b/packages/effect-machine/perf/types/transition-construction.ts @@ -1,32 +1,48 @@ +import { Schema } from "effect" import { Machine } from "../../dist/index.js" -import { machine, Root, Saved } from "./transition-construction-control.js" +import { Root, Saved, States } from "./transition-construction-control.js" + +const targets = Machine.targets(States) +const machine = Machine.make({ + branches: { reset: { idle: { target: targets.root.Idle }, same: { none: true } } }, + + root: States, + events: Machine.events({ Save: { text: Schema.String }, Retry: {}, Reset: {} }), + initial: (root) => root.from(() => ({ count: 0 })) +}) const handled = machine.handle({ - on: { - Reset: (to) => to.self.update.guard(({ current }) => current.count > 0).from(() => ({ count: 0 })) - }, + on: { Reset: { update: targets.root, guard: ({ root }) => root.count > 0, from: () => ({ count: 0 }) } }, states: { Idle: { on: { - Save: (to) => - to.local.Saved().updating(to.root).guard(({ event }) => event.text.length > 0) - .from(({ current, event }) => ({ target: { text: event.text }, update: { count: current.count + 1 } })) + Save: { + target: targets.root.Saved, + update: targets.root, + guard: ({ event }) => event.text.length > 0, + from: ({ root, event }) => ({ target: { text: event.text }, update: { count: root.count + 1 } }) + } } }, Saved: { on: { - Save: (to) => - to.local.Saved().updating(to.root).reenter().decoded(({ current, event }) => ({ + Save: { + target: targets.root.Saved, + update: targets.root, + reenter: true, + decoded: ({ root, event }) => ({ target: new Saved({ text: event.text }), - update: new Root({ count: current.count + 1 }) - })), - Retry: (to) => to.local.Saved().reenter().from(({ state }) => ({ text: state.text })), - Reset: (to) => - to.branches({ idle: { target: to.local.Idle() }, same: { target: to.none } }).reenter() - .resolve(({ state, select }) => state.text.length === 0 ? select.idle.from() : select.same()) + update: new Root({ count: root.count + 1 }) + }) + }, + Retry: { target: targets.root.Saved, reenter: true, from: ({ state }) => ({ text: state.text }) }, + Reset: { + branches: "reset", + reenter: true, + resolve: ({ state, select }) => state.text.length === 0 ? select.idle.from() : select.same() + } } } } }) - void Machine.planInitial(handled) diff --git a/packages/effect-machine/src/Machine.ts b/packages/effect-machine/src/Machine.ts index 251f8cd2..48cd2654 100644 --- a/packages/effect-machine/src/Machine.ts +++ b/packages/effect-machine/src/Machine.ts @@ -111,9 +111,9 @@ type IsAny = 0 extends (1 & A) ? true : false * * **Gotchas** * - * Use `.guard(predicate)` to decline a transition before constructing its next - * state. Use `.resolve(..., { declinable: true })` when conditional resolution - * needs to return `decline()`. Use `after` for cancellable state-scoped delays. + * Use `guard` to decline a transition before constructing its next state. + * Use `declinable: true` when a named branch resolver may return `decline()`. + * Register cancellable state-scoped delays in `make({ timers })`. * * **Example** * @@ -122,16 +122,19 @@ type IsAny = 0 extends (1 & A) ? true : false * import { Schema } from "effect" * * const events = Machine.events({ Add: { by: Schema.Number } }) + * const Root = Machine.state({ fields: { count: Schema.Number } }) + * const targets = Machine.targets(Root) * export const counter = Machine.make({ - * root: Machine.state({ fields: { count: Schema.Number } }), + * root: Root, * events, * initial: (root) => root.from(() => ({ count: 0 })) * }).handle({ * on: { - * Add: (to) => - * to.self.update.guard(({ event }) => event.by > 0).from(({ current, event }) => ({ - * count: current.count + event.by - * })) + * Add: { + * update: targets.root, + * guard: ({ event }) => event.by > 0, + * from: ({ root, event }) => ({ count: root.count + event.by }) + * } * } * }) * ``` @@ -246,7 +249,7 @@ export interface Machine< ) => Machine.TargetBuilder /** @internal */ - readonly handlers: Machine.StateConfigs, E, R> + readonly handlers: Readonly> /** @internal */ readonly initial: (...args: [...Machine.InputArgs]) => Machine.InitialResult @@ -278,7 +281,8 @@ export interface Definition< Output = never, Emits extends ReadonlyArray = readonly [], InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] + ParentEvents extends ReadonlyArray = readonly [], + Registrations extends Machine.Registrations = {} > extends Machine< States, @@ -306,12 +310,14 @@ export interface Definition< * **Example** * * ```ts + * const targets = Machine.targets(States) * const counter = definition.handle({ states: { * Count: { * on: { - * Increment: (to) => - * to.branch.Count().resolve(({ event, state, target }) => - * target.decoded(new Count({ value: state.value + event.by }))) + * Increment: { + * update: targets.root.Count, + * decoded: ({ event, state }) => new Count({ value: state.value + event.by }) + * } * } * } * } }) @@ -324,16 +330,13 @@ export interface Definition< Events, Emits, Input, - Machine.StateIdentifier, - never, - never, InitialE, InitialR, FinalStates, Output, - never, InputEvents, - ParentEvents + ParentEvents, + Registrations > } @@ -665,9 +668,6 @@ type IncompatibleRuntime = Requirements extends Run : never const InvokeTypeId: typeof internal.InvokeTypeId = internal.InvokeTypeId -const TransitionTypeId: typeof internal.TransitionTypeId = internal.TransitionTypeId -declare const TransitionBuilderTypeId: unique symbol -declare const InvokeBuilderTypeId: unique symbol declare const InitialBuilderTypeId: unique symbol type StateDefinitionError< @@ -4750,7 +4750,7 @@ export declare namespace Machine { * do not directly control state re-entry. Exit and entry paths are derived * from the previous and next active paths. Shared active ancestors remain * entered even when a `full` target supplies their values again. Use an event - * transition with `.reenter()` when the source should explicitly exit and + * transition with `reenter: true` when the source should explicitly exit and * enter again. * * @category models @@ -4841,206 +4841,8 @@ export declare namespace Machine { Scope extends Topology.TargetSelectionScope | undefined = Topology.TargetSelectionScope | undefined > = () => SelectionValue - type InitialSelectionMethod< - Builder, - Path extends string, - Scope extends Topology.TargetSelectionScope - > = Builder extends { readonly initial: infer Initial } ? { - readonly initial: SelectionValue - } - : {} - - type SelectionTreeWithPrefix< - AllStates extends StateSchemas, - States extends StateSchemas, - Prefix extends string, - Scope extends "local" | "branch", - Builder - > = { - readonly [Key in Extract | ChoiceStateKey, keyof Builder>]: SelectionNode< - AllStates, - States[Key], - JoinPath, - Scope, - Builder[Key] - > - } - - type SelectionNode< - AllStates extends StateSchemas, - Node, - Path extends string, - Scope extends "local" | "branch", - Builder - > = Node extends ChoiceStateNodeConfig ? SelectionMethod< - Builder, - Path, - "choice", - Scope - > - : Node extends { readonly states: infer Children extends StateSchemas } ? - & SelectionMethod - & InitialSelectionMethod - & SelectionTreeWithPrefix - : SelectionMethod - - /** @internal */ - type StateUpdateSelectionForNode< - AllStates extends StateSchemas, - Node, - Path extends string, - Scope extends "local" | "branch" - > = Node extends { readonly type: "final" } ? {} : NodeSchema extends never ? {} - : { - readonly update: SelectionValue< - StateUpdateBuilder>>, - Path, - "update", - Scope - > - } - - /** @internal */ - type BranchUpdateSelectionPath< - AllStates extends StateSchemas, - Node, - Path extends string, - Rest extends string, - Scope extends "local" | "branch" = "branch" - > = - & StateUpdateSelectionForNode - & (Node extends { readonly states: infer Children extends StateSchemas } ? - Rest extends `${infer Head}.${infer Tail}` ? Head extends keyof Children ? { - readonly [Key in Head]: BranchUpdateSelectionPath< - AllStates, - Children[Head], - JoinPath, - Tail, - Scope - > - } - : {} - : Rest extends keyof Children ? { - readonly [Key in Rest]: StateUpdateSelectionForNode< - AllStates, - Children[Rest], - JoinPath, - Scope - > - } - : {} - : {}) - - type FullSelectionNode< - AllStates extends StateSchemas, - Node, - Path extends StateIdentifier, - Builder - > = Node extends { readonly states: StateSchemas } ? - & SelectionMethod - & InitialSelectionMethod - : SelectionMethod - type RootBuilder = Builder extends { readonly "": infer Root } ? Root : never - type FullTargetSelector = - & SelectionValue>, "", "state", "full"> - & InitialSelectionMethod>, "", "full"> - - type BranchTargetSelector> = - States[""] extends { readonly states: infer Children extends StateSchemas } ? - & SelectionTreeWithPrefix>> - & (Source extends ChoiceIdentifier ? {} - : Omit, "update">) - : {} - - type LocalTargetSelector< - States extends StateSchemas, - Source extends StateNodeIdentifier - > = NearestCompoundScope extends infer Scope ? [Scope] extends [never] ? {} - : Scope extends StateIdentifier ? - ChildrenOf extends infer Children extends StateSchemas ? - LocalTargetBuilder extends infer Builder ? - & SelectionTreeWithPrefix - & ("with" extends keyof Builder ? { - readonly with: SelectionValue - } - : {}) - & (Source extends ChoiceIdentifier ? {} - : Scope extends ValuedStateIdentifier ? { - readonly update: SelectionValue, Scope, "update", "local"> - } - : {}) - : {} - : {} - : {} - : {} - - type HistorySelectionTree< - AllStates extends StateSchemas, - States extends StateSchemas, - Prefix extends string, - Builder - > = { - readonly [Key in Extract, keyof Builder>]: States[Key] extends HistoryStateNodeConfig ? - SelectionValue< - Builder[Key], - JoinPath, - "history", - "full" - > - : States[Key] extends { readonly states: infer Children extends StateSchemas } ? HistorySelectionTree< - AllStates, - Children, - JoinPath, - Builder[Key] - > - : never - } - - /** - * Definition-time topology selector available to an ordinary transition. - * - * Topology-only instructions (`none`, declared `initial` and history - * selections, and `local.with`) are values. - * - * State and choice destinations remain callable selection methods so their - * target-specific resolver APIs retain exact inference. - */ - export interface TargetSelector< - States extends StateSchemas, - Source extends StateNodeIdentifier - > { - /** Replaces this handler owner's value without reentry. */ - /** Selects the current handler owner for a value update without reentry. */ - readonly self: Source extends ValuedStateIdentifier - ? StateUpdateSelectionForNode, Source, "branch"> - : {} - /** Addresses the root and its retained value from any active descendant. */ - readonly root: "" extends ActiveStateKey ? - & SelectionNode< - States, - States[""], - "", - "branch", - (BranchTargetBuilder extends { readonly "": infer Builder } ? Builder : never) - > - & StateUpdateSelectionForNode - : never - /** Handles the trigger without selecting a destination. */ - readonly none: SelectionValue["none"], never, "none", "local"> - /** Selects a destination or updates the nearest active compound scope. */ - readonly local: LocalTargetSelector - /** Selects a destination or updates a valued active ancestor under the current root. */ - readonly branch: BranchTargetSelector - /** Selects a complete destination under any top-level state. */ - readonly full: FullTargetSelector - /** Restores a shallow or deep history pseudo-state. */ - readonly history: States[""] extends { readonly states: infer Children extends StateSchemas } - ? HistorySelectionTree>> - : {} - } - /** Definition-time selector that can choose only a valid top-level initial entry. */ type InitialTargetSelector = { readonly [Key in Extract, keyof InitialBuilder>]: States[Key] extends @@ -5078,13 +4880,8 @@ export declare namespace Machine { /** Event that selected this handler, narrowed by its `_tag`. */ readonly event: EventByTag - /** - * Provides typed builders for choosing the next active state from this - * handler. Each builder documents which existing state values it keeps. - * - * @since 0.4.0 - */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> } /** @@ -5129,6 +4926,8 @@ export declare namespace Machine { readonly children: ChildOwner> /** Value owned by the state that owns this invocation. */ readonly state: StateByIdentifier + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> /** Value owned by the nearest schema-backed ancestor, when one exists. */ readonly containingState: ParentStateValue /** Schema-backed ancestor values keyed by their complete state paths. */ @@ -5162,8 +4961,8 @@ export declare namespace Machine { readonly containingState: ParentStateValue /** Schema-backed ancestor values keyed by their complete state paths. */ readonly ancestors: ParentStateValues - /** Builders for selecting the owning machine's next state. */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> /** Latest active lifecycle snapshot published by the invoked logic or child. */ readonly snapshot: Extract, { readonly status: "active" }> } @@ -5193,8 +4992,8 @@ export declare namespace Machine { readonly ancestors: ParentStateValues /** Complete owning-machine configuration captured for this transition. */ readonly snapshot: Snapshot - /** Builders for selecting the owning machine's next state. */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> /** Successful output produced by the invocation. */ readonly output: Output } @@ -5219,8 +5018,8 @@ export declare namespace Machine { readonly ancestors: ParentStateValues /** Complete owning-machine configuration captured for this transition. */ readonly snapshot: Snapshot - /** Builders for selecting the owning machine's next state. */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> /** Next element emitted by the invoked Stream. */ readonly element: Element } @@ -5245,8 +5044,8 @@ export declare namespace Machine { readonly ancestors: ParentStateValues /** Complete owning-machine configuration captured for this transition. */ readonly snapshot: Snapshot - /** Builders for selecting the owning machine's next state. */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> /** Typed failure produced by the invocation. */ readonly error: Error } @@ -5276,14 +5075,8 @@ export declare namespace Machine { /** Lifecycle event retained while the eventless transition is evaluated. */ readonly event: LifecycleEvent - /** - * Provides typed builders for choosing the next active state from this - * eventless handler. Each builder documents which existing state values it - * keeps. - * - * @since 0.4.0 - */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> } /** @@ -5313,14 +5106,8 @@ export declare namespace Machine { /** Output produced by the completed final child or parallel regions. */ readonly output: CompletionOutputByIdentifier - /** - * Provides typed builders for choosing the next active state after this - * state completes. Each builder documents which existing state values it - * keeps. - * - * @since 0.4.0 - */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> } /** Context passed to a transient choice resolver. There is no `state` value. */ @@ -5346,8 +5133,8 @@ export declare namespace Machine { } /** Lifecycle event that led to the transient choice. */ readonly event: LifecycleEvent - /** Builders for selecting the concrete destination of this choice. */ - readonly target: TargetBuilder + /** Value owned by the logical root. */ + readonly root: StateByIdentifier>> } /** @@ -5444,7 +5231,7 @@ export declare namespace Machine { * * Handlers return snapshots for complete state replacement, target builder * results for path-safe partial transitions, state-value updates, or - * an explicit targetless result. In a machine definition, select `to.none`; + * an explicit targetless result. In a machine definition, select `{ none: true }`; * its optional resolver returns `undefined`. Raw decoded state values are * not accepted as transition targets. * @@ -5542,8 +5329,8 @@ export declare namespace Machine { * @category utility types * @since 0.4.0 */ - export type EventTransitionReturn = Transition extends (...args: any) => infer Authored ? - Authored extends TransitionBuilderEvidence ? Result : never + export type EventTransitionReturn = Transition extends + { readonly initial: TargetReference } ? InitialTarget

: Transition extends { readonly resolve?: infer Resolve } ? NonNullable extends (...args: any) => infer Ret ? Ret : never : never @@ -5578,7 +5365,9 @@ export declare namespace Machine { * @category utility types * @since 0.4.0 */ - export type InvokeResolvedSource = Source extends (...args: any) => infer Resolved ? Resolved : Source + export type InvokeResolvedSource = Source extends LazyProgramValue ? Source + : Source extends (...args: any) => infer Resolved ? Resolved + : Source type InvokeFactoryResult = Source extends (...args: any) => infer Resolved ? Resolved : never @@ -5633,8 +5422,7 @@ export declare namespace Machine { export type InvokeInitialError = Invoke extends { readonly [InvokeTypeId]: { readonly initialError: Types.Covariant } } ? InitialError - : InvokeLogic extends Logic ? InitialError - : never + : LogicInitialErrorOf> /** * Extracts the runtime error from an invoked child process. * @@ -5644,8 +5432,7 @@ export declare namespace Machine { export type InvokeRuntimeError = Invoke extends { readonly [InvokeTypeId]: { readonly error: Types.Covariant } } ? Error - : InvokeLogic extends Logic ? Error - : never + : LogicErrorOf> /** * Extracts the output from an invoked child process. * @@ -5655,8 +5442,7 @@ export declare namespace Machine { export type InvokeOutput = Invoke extends { readonly [InvokeTypeId]: { readonly output: Types.Covariant } } ? Output - : InvokeLogic extends Logic ? Output - : never + : LogicOutputOf> /** * Extracts the service requirements from an invoke source child process logic. * @@ -5666,8 +5452,7 @@ export declare namespace Machine { export type InvokeServices = Invoke extends { readonly [InvokeTypeId]: { readonly requirements: Types.Covariant } } ? Requirements - : InvokeLogic extends Logic ? Requirements - : never + : LogicServicesOf> /** * Extracts events emitted directly by an invoked child. * @@ -5768,24 +5553,8 @@ export declare namespace Machine { | Effect.Services> | InvokeRequirements - /** Type evidence retained by an authored transition without affecting runtime data. */ - export interface TransitionTyped< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance = "required" - > { - readonly [TransitionTypeId]: { - readonly owner: Types.Covariant - readonly acceptance: Types.Covariant - } - } - /** The only transition value accepted by machine handler APIs. */ - export type TransitionConfig< + type TransitionConfig< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, @@ -5793,13 +5562,12 @@ export declare namespace Machine { Context, Reenter extends boolean = false, Acceptance extends TransitionAcceptance = "required" - > = TransitionBuilderInput + > = Transition export type SelectionBuilder = Selection extends TargetSelection ? Builder : never export type SelectionKind = Selection extends TargetSelection ? Kind : never export type SelectionPath = Selection extends TargetSelection ? Path : never - type SelectionScope = Selection extends TargetSelection ? Scope : never export type TargetBuilderResult = | (Builder extends (...args: any) => infer Result ? Result : never) | (Builder extends { readonly decoded: (...args: any) => infer Result } ? Result : never) @@ -5808,20 +5576,9 @@ export declare namespace Machine { TargetBuilderResult : never - type RetainedUpdateOwner< - States extends StateSchemas, - Source extends StateNodeIdentifier, - Selection - > = Extract< - ParentStateIdentifier, - ParentStateIdentifier, string>> & ValuedStateIdentifier - > - - type RetainedOwnerSelector = () => TargetSelection - /** * Destination construction returned when a transition declares one retained - * valued owner with `.updating(...)`. + * valued owner with a `{ target, update }` declaration. * * @category models * @since 0.22.0 @@ -5873,83 +5630,12 @@ export declare namespace Machine { & Omit & (SelectionKind extends "none" ? {} : { readonly target: SelectionBuilder }) - type StateUpdateResolveContext< - States extends StateSchemas, - Context, - Owner extends ValuedStateIdentifier - > = Omit & { - /** Decoded owner value from the pre-transition snapshot. */ - readonly current: StateByIdentifier - /** Constructs the complete replacement for the selected owner. */ - readonly owner: StateUpdateBuilder - } - - type UpdatingTransitionResolveContext< - States extends StateSchemas, - Context, - Selection, - Owner extends ValuedStateIdentifier - > = Omit & { - /** Decoded owner value from the pre-transition snapshot. */ - readonly current: StateByIdentifier - /** Constructs the selected topology and requires `.update(...)`. */ - readonly target: UpdatingTargetBuilder, States, Owner> - /** Constructs the complete replacement for the retained owner. */ - readonly owner: StateUpdateBuilder - } - /** Context capability available only to explicitly declinable resolvers. */ export interface DeclineCapability { /** Declines this candidate and continues hierarchical transition selection. */ readonly decline: () => Declined } - export type TransitionResolver< - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Selection - > = ( - context: TransitionResolveContext, - enqueue: Enqueue, EmittedEventOf> - ) => SelectionKind extends "none" ? undefined : SelectedTargetResult | undefined - - export type DeclinableTransitionResolver< - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Selection - > = ( - context: TransitionResolveContext & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => - | (SelectionKind extends "none" ? undefined : SelectedTargetResult | undefined) - | Declined - - /** @internal */ - type StateUpdateResolver< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Owner extends ValuedStateIdentifier - > = ( - context: StateUpdateResolveContext, - enqueue: Enqueue, EmittedEventOf> - ) => StateUpdate - - /** @internal */ - type DeclinableStateUpdateResolver< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Owner extends ValuedStateIdentifier - > = ( - context: StateUpdateResolveContext & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => StateUpdate | Declined - /** One named destination declared by a branching transition. */ export interface TransitionBranchInput< Selection extends TargetSelection = TargetSelection @@ -5968,14 +5654,29 @@ export declare namespace Machine { } } + type CompletedBranchResult = R extends UpdatingStateConstruction + ? CombinedTarget, S, Owner> + : R + type SelectedConstruction = R extends + UpdatingStateConstruction ? { + readonly update: SelectedBranchBuilder, K> extends infer B ? { + readonly [M in keyof B]: StateUpdateBuilder[Extract>] extends + (...args: infer A) => unknown + ? (...args: A) => SelectedBranch, S, Owner>> + : never + } : + never + } + : SelectedBranch type SelectedBranchCallable = Callable extends { (...args: infer Arguments1): infer Result1 (...args: infer Arguments2): infer Result2 } ? { - (...args: Arguments1): SelectedBranch - (...args: Arguments2): SelectedBranch + (...args: Arguments1): SelectedConstruction + (...args: Arguments2): SelectedConstruction } - : Callable extends (...args: infer Arguments) => infer Result ? (...args: Arguments) => SelectedBranch + : Callable extends (...args: infer Arguments) => infer Result ? + (...args: Arguments) => SelectedConstruction : {} type SelectedBranchBuilderProperties = { @@ -5999,7 +5700,7 @@ export declare namespace Machine { export type BranchSelectionResult>> = { readonly [Key in Extract]: SelectedBranch< Key, - SelectedTargetResult + CompletedBranchResult> > }[Extract] @@ -6008,108 +5709,6 @@ export declare namespace Machine { Branches extends Readonly> > = Omit & { readonly select: BranchSelectors } - export type TransitionBranchesResolver< - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Branches extends Readonly> - > = ( - context: TransitionBranchesResolveContext, - enqueue: Enqueue, EmittedEventOf> - ) => BranchSelectionResult - - export type DeclinableTransitionBranchesResolver< - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Context, - Branches extends Readonly> - > = ( - context: TransitionBranchesResolveContext & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => BranchSelectionResult | Declined - - /** @internal Type evidence retained by a transition authored through its bound selector. */ - export interface TransitionBuilderEvidence { - readonly [TransitionBuilderTypeId]: { - readonly result: Types.Covariant - readonly acceptance: Types.Covariant - } - } - - type TransitionRequiredOptions = { - /** Keeps the transition required. The resolver cannot return `decline()`. */ - readonly declinable?: false - readonly reenter?: never - } - - type TransitionDeclinableOptions = { - /** Adds `decline()` to the resolver context and permits declining this candidate. */ - readonly declinable: true - readonly reenter?: never - } - - type BuiltTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Result, - Acceptance extends TransitionAcceptance - > = - & TransitionTyped - & TransitionBuilderEvidence - - interface TransitionResolveRequired< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > { - ( - resolve: TransitionResolver, - options?: TransitionRequiredOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectionKind extends "none" ? undefined : SelectedTargetResult | undefined, - "required" - > - } - - interface TransitionResolveDeclinable< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > { - ( - resolve: DeclinableTransitionResolver, - options: TransitionDeclinableOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - | (SelectionKind extends "none" ? undefined : SelectedTargetResult | undefined) - | Declined, - "declinable" - > - } - type ConstructionCallbacks = { readonly [ Method in Extract as Builder[Method] extends (...args: infer Args) => unknown @@ -6121,499 +5720,24 @@ export declare namespace Machine { never } - type GuardedTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > = - & ConstructionCallbacks< - Omit, - SelectionBuilder, - BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - > - & (SelectionSupportsDefaultConstruction extends true ? BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - : {}) + /** @internal Type evidence retained by a machine initial-entry declaration. */ + export interface InitialBuilderEvidence { + readonly [InitialBuilderTypeId]: Types.Covariant + } + + /** Decoded input shared by initial construction callbacks. */ + export interface InitialContext { + /** Decoded value supplied when the machine is started. */ + readonly input: Input + } + + /** A selected machine initial entry with its exact resolver target. */ + export type InitialTransitionTarget> = + & Selection + & ConstructionCallbacks, SelectionBuilder, InitialBuilderEvidence> + & (SelectionSupportsDefaultConstruction extends true ? InitialBuilderEvidence : {}) & { - readonly resolve: ( - resolve: TransitionResolver, - options?: TransitionRequiredOptions - ) => BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - } - - /** - * A selected transition target with target-specific resolver operations. - * - * **Example** (Constructing state while reentering) - * - * ```ts - * Reset: (to) => - * to.branch.Ready().reenter().from(({ event }) => ({ count: event.count })) - * ``` - * - * @inlineType TransitionRequiredOptions - * @inlineType TransitionDeclinableOptions - */ - export type TransitionTarget< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance, - Selection extends TargetSelection - > = - & Selection - & ("declinable" extends Acceptance ? { - /** Declines this candidate before construction when the predicate is false. */ - readonly guard: ( - predicate: (context: Omit) => boolean - ) => GuardedTransition - } : - {}) - & ConstructionCallbacks< - Omit, - SelectionBuilder, - BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult, - "required" - > - > - & (SelectionSupportsDefaultConstruction extends true ? BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectionKind extends "none" ? undefined : SelectedTargetResult | undefined, - "required" - > - : {}) - & { - /** Evaluates state construction after this transition is selected. */ - readonly resolve: - & TransitionResolveRequired - & ("declinable" extends Acceptance ? TransitionResolveDeclinable< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Selection - > - : {}) - } - & ([Reenter] extends [true] ? { - /** Forces source exit and entry, preserving subsequent construction and guards. */ - readonly reenter: () => Omit< - TransitionTarget, - typeof Topology.TargetSelectionTypeId - > - } : - {}) - & (SelectionKind extends "state" ? - SelectionScope extends "local" | "branch" ? - RetainedUpdateOwner extends infer Owner extends ValuedStateIdentifier ? - [Owner] extends [never] ? {} - : { - /** Declares one valued owner retained by the selected topology. */ - readonly updating: ( - owner: RetainedOwnerSelector - ) => UpdatingTransitionTarget< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Selection, - SelectedOwner - > - } - : {} - : {} - : {}) - - type UpdatingConstructionCallbacks = { - readonly [ - Method in Extract as Builder[Method] extends - (...args: infer Args) => unknown ? Args extends readonly [unknown?] ? Method : never : never - ]: Builder[Method] extends (...args: infer Args) => unknown ? - OwnerBuilder[Method] extends (value: infer Update) => unknown ? - (construct: (context: Context) => { readonly target: Args[0]; readonly update: Update }) => Result - : never : - never - } - - type UpdatingCallbacks< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection, - Owner extends ValuedStateIdentifier, - Acceptance extends TransitionAcceptance - > = UpdatingConstructionCallbacks< - Omit, "owner" | "target">, - SelectionBuilder, - StateUpdateBuilder, - BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - | CombinedTarget>, States, Owner> - | (Acceptance extends "declinable" ? Declined : never), - Acceptance - > - > - - type GuardedUpdatingTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection, - Owner extends ValuedStateIdentifier - > = UpdatingCallbacks & { - readonly resolve: ( - resolve: ( - context: UpdatingTransitionResolveContext, - enqueue: Enqueue, EmittedEventOf> - ) => CombinedTarget>, States, Owner>, - options?: TransitionRequiredOptions - ) => BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - CombinedTarget>, States, Owner> | Declined, - "declinable" - > - } - - /** - * A topology selection that requires one retained owner replacement. - * - * `.from` constructs both values from schema inputs; `.decoded` accepts both - * decoded values. Both callbacks return `{ target, update }` and read the same - * pre-transition snapshot. Use `.resolve` for explicit child construction, - * mixed construction methods, or queued commands. - * - * **Example** (Guarding an atomic destination and owner update) - * - * ```ts - * Save: (to) => to.local.Saving().updating(to.root) - * .guard(({ current }) => current.draft.length > 0) - * .from(({ current }) => ({ - * target: { text: current.draft }, - * update: { ...current, attempts: current.attempts + 1 } - * })) - * ``` - */ - export type UpdatingTransitionTarget< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance, - Selection extends TargetSelection, - Owner extends ValuedStateIdentifier - > = - & Selection - & UpdatingCallbacks - & ([Reenter] extends [true] ? { - readonly reenter: () => Omit< - UpdatingTransitionTarget, - typeof Topology.TargetSelectionTypeId - > - } : - {}) - & ("declinable" extends Acceptance ? { - /** Declines before either value is constructed or any commands are enqueued. */ - readonly guard: ( - predicate: ( - context: Omit, "owner" | "target"> - ) => boolean - ) => GuardedUpdatingTransition - } : - {}) - & { - /** @internal */ - readonly "~effect/Machine/UpdatingTransitionTarget": Owner - readonly resolve: - & (( - resolve: ( - context: UpdatingTransitionResolveContext, - enqueue: Enqueue, EmittedEventOf> - ) => CombinedTarget>, States, Owner>, - options?: TransitionRequiredOptions - ) => BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - CombinedTarget>, States, Owner>, - "required" - >) - & ("declinable" extends Acceptance ? ( - resolve: ( - context: UpdatingTransitionResolveContext & DeclineCapability, - enqueue: Enqueue, EmittedEventOf> - ) => CombinedTarget>, States, Owner> | Declined, - options: TransitionDeclinableOptions - ) => BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - CombinedTarget>, States, Owner> | Declined, - "declinable" - > - : {}) - } - - /** @internal */ - interface StateUpdateTransitionRequired< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > { - ( - resolve: StateUpdateResolver< - States, - Events, - Emits, - Context, - Extract, ValuedStateIdentifier> - >, - options?: TransitionRequiredOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult, - "required" - > - } - - /** @internal */ - interface StateUpdateTransitionDeclinable< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > { - ( - resolve: DeclinableStateUpdateResolver< - States, - Events, - Emits, - Context, - Extract, ValuedStateIdentifier> - >, - options: TransitionDeclinableOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - } - - type GuardedStateUpdateTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Selection extends TargetSelection - > = - & ConstructionCallbacks< - Omit< - StateUpdateResolveContext, ValuedStateIdentifier>>, - "owner" - >, - SelectionBuilder, - BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - > - & { - readonly resolve: ( - resolve: StateUpdateResolver< - States, - Events, - Emits, - Context, - Extract, ValuedStateIdentifier> - >, - options?: TransitionRequiredOptions - ) => BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - SelectedTargetResult | Declined, - "declinable" - > - } - - /** @internal */ - type StateUpdateTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance, - Selection extends TargetSelection - > = - & Selection - & ([Reenter] extends [true] ? { - /** Reenters the source while updating its retained valued ancestor. */ - readonly reenter: () => Omit< - StateUpdateTransition, - typeof Topology.TargetSelectionTypeId - > - } : - {}) - & ("declinable" extends Acceptance ? { - /** Declines before replacing the selected owner value. */ - readonly guard: ( - predicate: ( - context: Omit< - StateUpdateResolveContext< - States, - Context, - Extract, ValuedStateIdentifier> - >, - "owner" - > - ) => boolean - ) => GuardedStateUpdateTransition - } : - {}) - & ConstructionCallbacks< - Omit< - StateUpdateResolveContext, ValuedStateIdentifier>>, - "owner" - >, - SelectionBuilder, - BuiltTransition, "required"> - > - & { - readonly resolve: - & StateUpdateTransitionRequired - & ("declinable" extends Acceptance ? StateUpdateTransitionDeclinable< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Selection - > - : {}) - } - - /** @internal Type evidence retained by a machine initial-entry declaration. */ - export interface InitialBuilderEvidence { - readonly [InitialBuilderTypeId]: Types.Covariant - } - - /** Decoded input shared by initial construction callbacks. */ - export interface InitialContext { - /** Decoded value supplied when the machine is started. */ - readonly input: Input - } - - /** A selected machine initial entry with its exact resolver target. */ - export type InitialTransitionTarget> = - & Selection - & ConstructionCallbacks, SelectionBuilder, InitialBuilderEvidence> - & (SelectionSupportsDefaultConstruction extends true ? InitialBuilderEvidence : {}) - & { - /** Lazily constructs the selected initial state from decoded machine input. */ + /** Lazily constructs the selected initial state from decoded machine input. */ readonly resolve: ( resolve: (context: { /** Decoded value supplied when the machine is started. */ @@ -6683,917 +5807,150 @@ export declare namespace Machine { } ) => InitialBuilderEvidence> - type TransitionSelectorNode< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance, - Node - > = Node extends TargetSelection ? StateUpdateTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Node - > - : Node extends (...args: infer Args) => infer Selection ? Selection extends TargetSelection ? - & ((...args: Args) => TransitionTarget< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Selection - >) - & { - readonly [Key in keyof Node]: TransitionSelectorNode< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Node[Key] - > - } - : never - : Node extends TargetSelection ? TransitionTarget< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Node - > - : { - readonly [Key in keyof Node]: TransitionSelectorNode< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Node[Key] - > - } - - interface TransitionBranchesResolveRequired< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Branches extends Readonly> - > { - ( - resolve: TransitionBranchesResolver, - options?: TransitionRequiredOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - BranchSelectionResult, - "required" - > - } - - interface TransitionBranchesResolveDeclinable< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Branches extends Readonly> - > { - ( - resolve: DeclinableTransitionBranchesResolver, - options: TransitionDeclinableOptions - ): BuiltTransition< - States, - Events, - Emits, - StateId, - Context, - Reenter, - BranchSelectionResult | Declined, - "declinable" - > - } - - type TransitionSelectorTargets< - in out States extends StateSchemas, - in out Events extends ReadonlyArray, - in out Emits extends ReadonlyArray, - in out StateId extends StateNodeIdentifier, - in out Context, - in out Reenter extends boolean, - in out Acceptance extends TransitionAcceptance - > = { - readonly [Key in keyof TargetSelector]: TransitionSelectorNode< - States, - Events, - Emits, - StateId, - Context, - Key extends "self" ? false : Reenter, - Acceptance, - TargetSelector[Key] - > - } - - type TransitionBranchesTarget< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance, - Branches extends Readonly> - > = - & { - /** Resolves exactly one declared branch after this transition is selected. */ - readonly resolve: - & TransitionBranchesResolveRequired - & ("declinable" extends Acceptance - ? TransitionBranchesResolveDeclinable - : {}) - } - & ([Reenter] extends [true] ? { - readonly reenter: () => TransitionBranchesTarget< - States, - Events, - Emits, - StateId, - Context, - Reenter, - Acceptance, - Branches - > - } : - {}) - - /** Selector supplied to inline transition declarations. */ - export interface TransitionSelector< - in out States extends StateSchemas, - in out Events extends ReadonlyArray, - in out Emits extends ReadonlyArray, - in out StateId extends StateNodeIdentifier, - in out Context, - in out Reenter extends boolean, - in out Acceptance extends TransitionAcceptance - > extends TransitionSelectorTargets { - /** Declares a closed set of named destinations for one resolver. */ - readonly branches: >>( - branches: Branches & ValidateTransitionBranchRecord> - ) => TransitionBranchesTarget - } - - /** Inline transition declaration accepted by state and invocation handlers. */ - export type TransitionBuilderInput< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context, - Reenter extends boolean, - Acceptance extends TransitionAcceptance - > = ( - to: TransitionSelector - ) => - & TransitionTyped - & TransitionBuilderEvidence - export type InvokeTransition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateNodeIdentifier, - Context - > = TransitionConfig - - export type InvokeSource = Value | ((context: Context) => Value) - - /** Inline state-owned work that runs for the lifetime of its active state. */ - export interface InvokeOwned< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > { - readonly "~effect/Machine/InvokeOwner"?: Types.Covariant< - readonly [States, Events, Emits, StateId, InputEvents, ParentEvents] - > - } - - /** Type evidence retained by a completed state-owned invocation. */ - export interface InvokeTyped< - Output, - Error, - Requirements, - InitialError, - Emits = never, - ParentEvent = never, - Outcomes = never - > { - readonly [InvokeTypeId]: { - readonly output: Types.Covariant - readonly error: Types.Covariant - readonly requirements: Types.Covariant - readonly initialError: Types.Covariant - readonly emits: Types.Covariant - readonly parentEvents: Types.Covariant - readonly outcomes: Types.Covariant - } - } - - type StoredInvokeConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = - & InvokeOwned - & ( - | { - readonly id: string - readonly effect: ( - context: InvokeContext - ) => Effect.Effect - readonly stream?: never - readonly after?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onDone?: unknown - readonly onFailure?: unknown - readonly onElement?: never - readonly onSnapshot?: never - } - | { - readonly id: string - readonly stream: ( - context: InvokeContext - ) => Stream.Stream - readonly effect?: never - readonly after?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onElement?: unknown - readonly onDone: unknown - readonly onFailure?: unknown - readonly onSnapshot?: never - } - | { - readonly id: string - readonly after: InvokeSource< - Duration.Input, - InvokeContext - > - readonly effect?: never - readonly stream?: never - readonly logic?: never - readonly child?: never - readonly address?: never - readonly onDone: unknown - readonly onFailure?: never - readonly onElement?: never - readonly onSnapshot?: never - } - | { - readonly id: string - readonly address: string - readonly logic: InvokeSource< - Logic, - InvokeContext - > - readonly effect?: never - readonly stream?: never - readonly after?: never - readonly child?: never - readonly onDone?: unknown - readonly onFailure?: unknown - readonly onElement?: never - readonly onSnapshot?: unknown - } - | { - readonly child: ChildMachine.Any - readonly input?: - | {} - | null - | ((context: InvokeContext) => unknown) - readonly id?: never - readonly address?: never - readonly effect?: never - readonly stream?: never - readonly after?: never - readonly logic?: never - readonly onDone?: unknown - readonly onFailure?: unknown - readonly onElement?: never - readonly onSnapshot?: unknown - } - ) - - type StoredInvokeDefinition< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = - | StoredInvokeConfig - | ReadonlyArray> - - type LogicInitialEffectOf = Value extends { readonly initial: infer Initial } ? - Initial extends (...args: ReadonlyArray) => infer Result ? Result : never - : never - - type LogicRunEffectOf = Value extends { readonly run: infer Run } ? - Run extends (...args: ReadonlyArray) => infer Result ? Result : never - : never - - export type LogicStateOf = Effect.Success> - export type LogicEventOf = Value extends { readonly initial: infer Initial } ? - Initial extends (scope: infer LogicScope, ...args: ReadonlyArray) => any ? - LogicScope extends Logic.Scope ? Event : never - : never - : never - export type LogicErrorOf = Effect.Error> - export type LogicServicesOf = Effect.Services | LogicRunEffectOf> - export type LogicOutputOf = Effect.Success> - export type LogicInitialErrorOf = Effect.Error> - - type RequiredInvokeChannel = IsAny extends true ? Channel - : [Value] extends [never] ? never - : Channel - - type InvokeBuilderResult< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Outcomes - > = - & InvokeOwned - & InvokeTyped - & { readonly [InvokeBuilderTypeId]: true } - - /** Lifecycle handlers exposed according to the selected invocation source. */ - type InvokeBuilder< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Element, - Pending extends string, - SnapshotHandler, - Outcomes = never - > = - & ([Pending] extends [never] ? InvokeBuilderResult< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Outcomes - > - : {}) - & ("done" extends Pending ? { - /** Handles successful completion and exposes the invocation output. */ - readonly onDone: < - const Handler extends InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext - > - >( - handler: - & Handler - & InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeDoneContext - > - ) => InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Element, - Exclude, - SnapshotHandler, - Outcomes | Handler - > - } - : {}) - & ("failure" extends Pending ? { - /** Handles typed failure and exposes the invocation error. */ - readonly onFailure: < - const Handler extends InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext - > - >( - handler: - & Handler - & InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeFailureContext - > - ) => InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Element, - Exclude, - SnapshotHandler, - Outcomes | Handler - > - } - : {}) - & ("element" extends Pending ? { - /** Handles each backpressured element emitted by an invoked Stream. */ - readonly onElement: < - const Handler extends InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeElementContext - > - >( - handler: - & Handler - & InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeElementContext - > - ) => InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Element, - Exclude, - SnapshotHandler, - Outcomes | Handler - > - } - : {}) - & ([SnapshotHandler] extends [never] ? {} : { - /** Handles each active snapshot published by invoked logic or a child machine. */ - readonly onSnapshot: (handler: Handler & SnapshotHandler) => InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error, - Requirements, - InitialError, - ChildEmits, - ChildParentEvent, - Element, - Pending, - never, - Outcomes | Handler - > - }) - - type EffectInvokeBuilder< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Source extends (...args: ReadonlyArray) => Effect.Effect - > = InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Effect.Success>, - Effect.Error>, - Effect.Services>, - never, - never, - never, - never, - | RequiredInvokeChannel>, "done"> - | RequiredInvokeChannel>, "failure">, - never - > - - type StreamInvokeBuilder< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Source extends (...args: ReadonlyArray) => Stream.Stream - > = InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - void, - Stream.Error>, - Stream.Services>, - never, - never, - never, - Stream.Success>, - | "done" - | RequiredInvokeChannel>, "element"> - | RequiredInvokeChannel>, "failure">, - never - > - - type LogicInvokeBuilder< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - ChildLogic - > = InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - LogicOutputOf, - LogicErrorOf, - LogicServicesOf, - LogicInitialErrorOf, - never, - never, - never, - | RequiredInvokeChannel, "done"> - | RequiredInvokeChannel, "failure">, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeSnapshotContext< - States, - Events, - Emits, - StateId, - LogicStateOf, - LogicErrorOf, - LogicOutputOf, - InputEvents, - ParentEvents - > - > - > - - type ChildInvokeBuilder< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Child extends ChildMachine.Any, - ChildDefinition extends Machine.Any = Child["machine"] - > = InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Output, - Error | ActionError>, - Services, - InitialError, - EmittedEvent, - EventOf>, - never, - | RequiredInvokeChannel, "done"> - | RequiredInvokeChannel | ActionError>, "failure">, - InvokeTransition< - States, - Events, - Emits, - StateId, - InvokeSnapshotContext< - States, - Events, - Emits, - StateId, - Snapshot>, - Error, - Output, - InputEvents, - ParentEvents - > - > - > - - /** - * Selects state-owned work and begins its lifecycle-handler chain. - * - * Each source exposes exactly the lifecycle methods that it can produce. A - * chain becomes returnable from `invoke` only after every reachable required - * channel has been handled. - * - * **Example** (Invoking an Effect) - * - * ```ts - * invoke: (from) => - * from.effect("load-user", () => loadUser).onDone((to) => - * to.branch.Ready().resolve(({ output, target }) => target.from({ user: output })) - * ).onFailure((to) => - * to.branch.Failed().resolve(({ error, target }) => target.from({ error })) - * ) - * ``` - * - * @inlineType EffectInvokeBuilder - * @inlineType StreamInvokeBuilder - * @inlineType LogicInvokeBuilder - * @inlineType ChildInvokeBuilder - * @inlineType InvokeBuilder - * - * @category models - * @since 0.18.0 - */ - export interface InvokeSelector< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > { - /** - * Starts a fresh Effect each time the owning state is entered. - * - * @param id Parent-local lifecycle identifier. - * @param source Lazy Effect factory evaluated on every entry. - */ - readonly effect: < - const Source extends ( - context: InvokeContext - ) => Effect.Effect - >( - id: InvokeLifecycleId, - source: Source - ) => EffectInvokeBuilder - - /** - * Starts a fresh, backpressured Stream each time the owning state is entered. - * - * @param id Parent-local lifecycle identifier. - * @param source Lazy Stream factory evaluated on every entry. - */ - readonly stream: < - const Source extends ( - context: InvokeContext - ) => Stream.Stream - >( - id: InvokeLifecycleId, - source: Source - ) => StreamInvokeBuilder - - /** - * Starts a cancellable state-scoped timer. - * - * @param id Parent-local lifecycle identifier. - * @param duration Duration input or context-dependent duration factory. - */ - readonly timer: ( - id: InvokeLifecycleId, - duration: InvokeSource< - Duration.Input, - InvokeContext - > - ) => InvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - void, - never, - never, - never, - never, - never, - never, - "done", - never - > - - /** - * Starts reusable process logic at a typed parent-local address. - * - * @param id Parent-local lifecycle identifier. - * @param options Address and reusable logic value or factory. - */ - readonly logic: { - < - const Source extends ( - context: InvokeContext - ) => unknown, - Address extends ChildAddress - >( - id: InvokeLifecycleId, - options: { - /** Typed parent-local address used to send events to this logic. */ - readonly address: - & Address - & ChildAddress.Compatibility>>> - /** Context-dependent factory that returns reusable process logic. */ - readonly logic: Source - }, - ..._validation: ReturnType extends { readonly initial: unknown; readonly run: unknown } ? [] : [ - "logic factory must return Machine.Logic" - ] - ): LogicInvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - ReturnType - > - >( - id: InvokeLifecycleId, - options: { - /** Typed parent-local address used to send events to this logic. */ - readonly address: Address & ChildAddress.Compatibility>> - /** Reusable process logic started when the owning state enters. */ - readonly logic: Source - }, - ..._validation: Source extends { readonly initial: unknown; readonly run: unknown } ? [] : [ - "logic must implement Machine.Logic" - ] - ): LogicInvokeBuilder< - States, - Events, - Emits, - StateId, - InputEvents, - ParentEvents, - Source - > - } + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateNodeIdentifier, + Context + > = TransitionConfig - /** - * Starts a complete child statechart represented by a reusable descriptor. - * - * @param child Reusable descriptor created with `Machine.child`. - * @param options Input construction for a child with a non-void input schema. - */ - readonly child: ( - child: - & Child - & (Child["machine"] extends EnsureExecutable< - Machine.States, - Machine.UnhandledStates, - Machine.OutputStates - > ? unknown - : never), - ...options: InputSchema extends typeof Schema.Void ? [options?: { readonly input?: never }] - : [options: { - /** Child input value or factory evaluated from the owning state context. */ - readonly input: InvokeSource< - Input, - InvokeContext - > - }] - ) => ChildInvokeBuilder + export type InvokeSource = Value | ((context: Context) => Value) + + /** Inline state-owned work that runs for the lifetime of its active state. */ + interface InvokeOwned< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray = Events, + ParentEvents extends ReadonlyArray = readonly [] + > { + readonly "~effect/Machine/InvokeOwner"?: Types.Covariant< + readonly [States, Events, Emits, StateId, InputEvents, ParentEvents] + > } - /** - * Inline invocation declaration accepted by an active state handler. - * - * Return one completed source chain or an array of completed chains. Source - * computations and child descriptors may be extracted, but the chain stays - * local to preserve the owning state and machine protocols. - * - * @category models - * @since 0.18.0 - */ - export type InvokeBuilderInput< + type StoredInvokeConfig< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, StateId extends StateIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > = ( - from: InvokeSelector - ) => - | (InvokeOwned & { - readonly [InvokeBuilderTypeId]: true - }) - | ReadonlyArray< - InvokeOwned & { - readonly [InvokeBuilderTypeId]: true + > = + & InvokeOwned + & ( + | { + readonly id: string + readonly effect: ( + context: InvokeContext + ) => Effect.Effect + readonly stream?: never + readonly after?: never + readonly logic?: never + readonly child?: never + readonly address?: never + readonly onDone?: unknown + readonly onFailure?: unknown + readonly onElement?: never + readonly onSnapshot?: never } - > + | { + readonly id: string + readonly stream: ( + context: InvokeContext + ) => Stream.Stream + readonly effect?: never + readonly after?: never + readonly logic?: never + readonly child?: never + readonly address?: never + readonly onElement?: unknown + readonly onDone: unknown + readonly onFailure?: unknown + readonly onSnapshot?: never + } + | { + readonly id: string + readonly after: InvokeSource< + Duration.Input, + InvokeContext + > + readonly effect?: never + readonly stream?: never + readonly logic?: never + readonly child?: never + readonly address?: never + readonly onDone: unknown + readonly onFailure?: never + readonly onElement?: never + readonly onSnapshot?: never + } + | { + readonly id: string + readonly address: string + readonly logic: InvokeSource< + Logic, + InvokeContext + > + readonly effect?: never + readonly stream?: never + readonly after?: never + readonly child?: never + readonly onDone?: unknown + readonly onFailure?: unknown + readonly onElement?: never + readonly onSnapshot?: unknown + } + | { + readonly child: ChildMachine.Any + readonly input?: + | {} + | null + | ((context: InvokeContext) => unknown) + readonly id?: never + readonly address?: never + readonly effect?: never + readonly stream?: never + readonly after?: never + readonly logic?: never + readonly onDone?: unknown + readonly onFailure?: unknown + readonly onElement?: never + readonly onSnapshot?: unknown + } + ) + + type StoredInvokeDefinition< + States extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + StateId extends StateIdentifier, + InputEvents extends ReadonlyArray = Events, + ParentEvents extends ReadonlyArray = readonly [] + > = + | StoredInvokeConfig + | ReadonlyArray> + + type LogicInitialEffectOf = Value extends { readonly initial: infer Initial } ? + Initial extends (...args: ReadonlyArray) => infer Result ? Result : never + : never + + type LogicRunEffectOf = Value extends { readonly run: infer Run } ? + Run extends (...args: ReadonlyArray) => infer Result ? Result : never + : never + + export type LogicStateOf = Effect.Success> + export type LogicEventOf = Value extends { readonly initial: infer Initial } ? + Initial extends (scope: infer LogicScope, ...args: ReadonlyArray) => any ? + LogicScope extends Logic.Scope ? Event : never + : never + : never + export type LogicErrorOf = Effect.Error> + export type LogicServicesOf = Effect.Services | LogicRunEffectOf> + export type LogicOutputOf = Effect.Success> + export type LogicInitialErrorOf = Effect.Error> /** Output construction available to final and output-producing parallel states. */ type OutputHandlerConfig< @@ -7623,86 +5980,6 @@ export declare namespace Machine { readonly output?: never } - /** - * Configuration accepted for a non-final state. - * - * **Example** (State actions, events, and invocation) - * - * ```ts - * machine.handle({ states: { - * Loading: { - * entry: (_, enqueue) => enqueue.emit({ _tag: "Started" }), - * invoke: (from) => - * from.effect("load", () => load).onDone((to) => to.branch.Ready()), - * on: { Cancel: (to) => to.branch.Idle() } - * } - * } }) - * ``` - * - * @inlineType ActiveOutputHandlerConfig - * @inlineType OutputHandlerConfig - * - * @category models - * @since 0.4.0 - */ - export type ActiveStateConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - E, - R, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = { - /** Runs synchronously when the state is entered and may enqueue commands. */ - readonly entry?: ( - context: StateActionContext, - enqueue: Enqueue, EmittedEventOf> - ) => StateActionResult - /** Runs synchronously before the state is exited and may enqueue commands. */ - readonly exit?: ( - context: StateActionContext, - enqueue: Enqueue, EmittedEventOf> - ) => StateActionResult - /** Starts state-owned Effect, Stream, timer, logic, or child lifecycles. */ - readonly invoke?: InvokeBuilderInput - /** Eventless transition evaluated after the state becomes stable. */ - readonly always?: TransitionConfig< - States, - Events, - Emits, - StateId, - AlwaysContext, - false, - TransitionAcceptance - > - /** Transition evaluated after this compound or parallel state completes. */ - readonly onDone?: TransitionConfig< - States, - Events, - Emits, - StateId, - DoneContext, - false, - TransitionAcceptance - > - /** Event handlers keyed by the `_tag` of the machine's public or internal events. */ - readonly on?: { - readonly [EventTag in TagOf]?: TransitionConfig< - States, - Events, - Emits, - StateId, - HandlerContext, - true, - TransitionAcceptance - > - } - /** Supplies missing direct initial-child values required by implicit entry and shallow history. */ - readonly initialize?: StateInitializeHandler - } & ActiveOutputHandlerConfig - /** * Values constructed for a state's schema-valued direct initial children. * @@ -7881,96 +6158,35 @@ export declare namespace Machine { readonly [Key in HistoryStateKey]?: HistoryDefaultEntry } - /** Required implementation for a choice pseudo-state. */ - export interface ChoiceStateConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - ChoiceId extends ChoiceIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > { - /** Required transition that resolves this transient choice to a concrete destination. */ - readonly choice: TransitionConfig< - States, - Events, - Emits, - ChoiceId, - ChoiceContext - > - readonly entry?: never - readonly exit?: never - readonly invoke?: never - readonly always?: never - readonly on?: never - readonly onDone?: never - readonly output?: never - readonly initialize?: never - } - - /** - * Configuration accepted for a final state. - * - * @inlineType OutputHandlerConfig - * - * @category models - * @since 0.4.0 - */ - export type FinalStateConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = { - /** Runs synchronously when the final state is entered and may enqueue commands. */ - readonly entry?: ( - context: StateActionContext, - enqueue: Enqueue, EmittedEventOf> - ) => StateActionResult - readonly exit?: never - readonly always?: never - readonly onDone?: never - readonly on?: never - } & OutputHandlerConfig> - - /** - * Configuration accepted by `handle` for a state tag. - * - * @category models - * @since 0.4.0 - */ - export type HandlerConfig< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - StateId extends StateIdentifier, - E, - R, - InputEvents extends ReadonlyArray = Events, - ParentEvents extends ReadonlyArray = readonly [] - > = NodeByIdentifier extends { readonly type: "final" } ? - FinalStateConfig - : ActiveStateConfig - - type HandlerNodeConfig< + /** Required implementation for a choice pseudo-state. */ + interface ChoiceStateConfig< States extends StateSchemas, Events extends ReadonlyArray, Emits extends ReadonlyArray, - Path extends StateNodeIdentifier, - E, - R, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray - > = Path extends ChoiceIdentifier ? ChoiceStateConfig - : Path extends StateIdentifier ? HandlerConfig - : never + ChoiceId extends ChoiceIdentifier, + InputEvents extends ReadonlyArray = Events, + ParentEvents extends ReadonlyArray = readonly [] + > { + /** Required transition that resolves this transient choice to a concrete destination. */ + readonly choice: TransitionConfig< + States, + Events, + Emits, + ChoiceId, + ChoiceContext + > + readonly entry?: never + readonly exit?: never + readonly invoke?: never + readonly always?: never + readonly on?: never + readonly onDone?: never + readonly output?: never + readonly initialize?: never + } type HandlerChildren = Node extends { readonly states: infer Children extends StateSchemas } ? Children : never - type HandlerNodeId = Extract> - type HandlerConfigPart = { readonly [Key in keyof Config as Key extends "states" ? never : Key]: Config[Key] } @@ -8013,78 +6229,6 @@ export declare namespace Machine { } : { readonly [Key in Path]?: Validation } - /** Nested handler-tree structure shared by active and final state configs. */ - type HandlerNode< - AllStates extends StateSchemas, - Node, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - E, - R, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - StateId extends StateNodeIdentifier - > = - & HandlerNodeConfig - & (StateId extends ChoiceIdentifier ? { - readonly states?: never - readonly history?: never - } - : HandlerChildren extends infer Children extends StateSchemas ? [Children] extends [never] ? { - readonly states?: never - readonly history?: never - } - : { - /** Child-state handlers nested according to the declared state topology. */ - readonly states?: HandlerTree< - AllStates, - Children, - Events, - Emits, - E, - R, - InputEvents, - ParentEvents, - Extract> - > - /** First-use defaults keyed by direct history pseudo-state. */ - readonly history?: HistoryDefaultConfig< - AllStates, - Events, - Emits, - Extract>, - Children - > - } - : { - readonly states?: never - readonly history?: never - }) - - type HandlerTree< - AllStates extends StateSchemas, - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - E, - R, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Prefix extends string - > = { - readonly [Key in ActiveStateKey | ChoiceStateKey]?: HandlerNode< - AllStates, - States[Key], - Events, - Emits, - E, - R, - InputEvents, - ParentEvents, - HandlerNodeId> - > - } - type HandlerNodeConfigKey = | "always" | "choice" @@ -8226,30 +6370,6 @@ export declare namespace Machine { > : never - type HandlerInitializeValidationAtPath = HandlerConfigAtPath extends - infer StateConfig ? [StateConfig] extends [never] ? HandlerValidationAtPath - }> - : "initialize" extends keyof StateConfig ? never - : HandlerValidationAtPath - }> - : never - - type HandlerInitialTargetValidationForPaths< - AllStates extends StateSchemas, - Config, - Required - > = Types.UnionToIntersection< - Required extends string ? HandlerInitializeValidationAtPath : unknown - > - type HandlerTreeInitialTargetPath = string extends keyof Config ? never : Config extends object ? { readonly [Key in keyof Config]: @@ -8258,16 +6378,6 @@ export declare namespace Machine { }[keyof Config] : never - type HandlerInitialTargetValidation< - AllStates extends StateSchemas, - Config - > = HandlerTreeInitialTargetPath extends infer TargetPath ? HandlerInitialTargetValidationForPaths< - AllStates, - Config, - RequiredInitializersForTargetPath - > - : unknown - type HandlerChildrenValidation< Node, Prefix extends string, @@ -8398,64 +6508,6 @@ export declare namespace Machine { > = [Incompatible] extends [never] ? unknown : HandlerValidationError<"Handler config requires an incompatible machine runtime", StateId, Incompatible> - type HandlerNodeValidationAtPath< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - Config, - AvailableOutputStates extends StateIdentifier, - StateId extends StateNodeIdentifier, - NodeConfig = HandlerConfigAtPath - > = [NodeConfig] extends [never] ? never - : HandlerNodeValidation< - AllStates, - HandlerNodeByPath, - Events, - InputEvents, - Emits, - StateId, - NodeConfig, - AvailableOutputStates - > extends infer Validation ? unknown extends Validation ? never - : "" extends keyof AllStates ? StateId extends "" ? { readonly ""?: Validation } - : { readonly ""?: { readonly states: HandlerValidationAtPath } } - : HandlerValidationAtPath - : never - - type HandlerTreeNodeValidations< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - Config, - AvailableOutputStates extends StateIdentifier - > = Types.UnionToIntersection< - StateNodeIdentifier extends infer StateId extends StateNodeIdentifier ? - StateId extends StateNodeIdentifier ? HandlerNodeValidationAtPath< - AllStates, - Events, - InputEvents, - Emits, - Config, - AvailableOutputStates, - StateId - > - : never - : never - > - - type HandlerTreeValidation< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - Config, - AvailableOutputStates extends StateIdentifier - > = - & HandlerUnknownStateKeyValidation - & HandlerTreeNodeValidations - type RootInitialTargetValidation = RequiredInitializersForTargetPath> extends infer Required ? Types.UnionToIntersection< @@ -8589,128 +6641,997 @@ export declare namespace Machine { NodeConfig > : never - readonly error: HandlerConfigError> - readonly services: ConfigServices> - readonly outputState: StateId extends StateIdentifier ? HandlerOutputStates< - AllStates, - StateId, - HandlerConfigPart - > + readonly error: HandlerConfigError> + readonly services: ConfigServices> + readonly outputState: StateId extends StateIdentifier ? HandlerOutputStates< + AllStates, + StateId, + HandlerConfigPart + > + : never + } + + // Compute every accumulated channel from one normalized node union instead + // of repeating recursive walks for states, errors, services, and outputs. + type HandlerTreeEvidence = StateNodeIdentifier extends + infer StateId extends StateNodeIdentifier ? + StateId extends StateNodeIdentifier ? HandlerNodeEvidence + : never + : never + + type HandleTreeResult< + AllStates extends StateSchemas, + Events extends ReadonlyArray, + Emits extends ReadonlyArray, + Input extends Schema.Top, + UnhandledStates extends StateIdentifier, + E, + R, + InitialE, + InitialR, + FinalStates extends StateIdentifier, + Output, + OutputStates extends StateIdentifier, + InputEvents extends ReadonlyArray, + ParentEvents extends ReadonlyArray, + Config + > = Machine< + AllStates, + Events, + Input, + Exclude["stateId"]>, + E | HandlerTreeEvidence["error"], + ExcludeCompatibleRuntime< + R | HandlerTreeEvidence["services"], + EventOf, + EmittedEventOf + >, + InitialE, + InitialR, + FinalStates, + Output, + Emits, + OutputStates | Extract["outputState"], StateIdentifier>, + InputEvents, + ParentEvents + > + + type ObjectCompoundPath = { + readonly [P in StateIdentifier]: NodeByIdentifier extends { + /** Child handlers nested according to the declared root topology. */ + readonly states: StateSchemas + } ? P : + never + }[StateIdentifier] + type LazyProgramValue = Effect.Effect | Stream.Stream + + /** Registered lazy programs and their optional explicit input adapters. */ + export type Program = Value | ((...args: never[]) => Value) + export type EffectSources = Readonly>>> + export type StreamSources = Readonly>>> + export type TimerSources = Readonly>> + export type LogicSources = Readonly> + export type ValidateLogicSources = { + readonly [K in keyof Sources]: InvokeResolvedSource extends { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: (...args: never[]) => Effect.Effect + readonly run: (...args: never[]) => Effect.Effect + } ? unknown + : never + } + + export type ChildSources = Readonly> + export type ValidatePrograms = { + readonly [K in keyof Sources]: Sources[K] extends LazyProgramValue ? unknown : + Sources[K] extends (...args: never[]) => unknown ? Parameters["length"] extends 1 ? unknown : never + : unknown + } + type ReferenceRoot = Extract, StateNodeConfig> + type ReferenceAt | HistoryIdentifier> = TargetReference< + ReferenceRoot, + P, + P extends HistoryIdentifier ? "history" : P extends ChoiceIdentifier ? "choice" : "state" + > + type ReferenceUnion | HistoryIdentifier> = { + readonly [P in Paths]: ReferenceAt + }[Paths] + type DestinationPath = Exclude, ""> + type RootSchemas = { readonly "": Root } + + /** A topology declaration; its state values are constructed by a resolver. */ + export type BranchDeclaration> = + & { + /** Optional presentation label for this named branch. */ + readonly title?: string + } + & ( + | { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: ReferenceUnion> + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: ReferenceUnion> + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + } + | { + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: ReferenceUnion> + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + } + | { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: ReferenceUnion, "">> + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + } + | { + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history: ReferenceUnion> + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + } + | { + /** Accepts the event without selecting a new destination. */ + readonly none: true + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + } + ) + export type BranchDefinitions = Readonly< + Record>>> + > + /** Source programs and named topology declarations captured by a machine definition. */ + export interface Registrations { + /** Lazy Effects or functions with one required input, invoked by registered name. */ + readonly effects?: EffectSources + /** Lazy Streams or functions with one required input, invoked by registered name. */ + readonly streams?: StreamSources + /** Cancellable delays, supplied as durations or functions of one required input. */ + readonly timers?: TimerSources + /** Logic values or functions of one required input; their Effect channels remain inferred. */ + readonly logic?: LogicSources + /** Child machine descriptors registered for state-owned invocation. */ + readonly children?: ChildSources + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: Readonly>>> + } + type Registered = Kind extends keyof R ? NonNullable : {} + type AtBuilder = P extends "" ? T + : P extends `${infer H}.${infer Rest}` ? H extends keyof T ? AtBuilder : never + : P extends keyof T ? T[P] + : never + type BranchBuilderAt, P extends string> = AtBuilder< + RootBuilder>, + P + > + type RefPath = R extends TargetReference ? P : never + type SelectionOf, D> = D extends { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: infer Ref + } ? RefPath extends infer P extends StateNodeIdentifier ? D extends { + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: infer Update + } ? + RefPath extends + infer Owner extends Extract, ParentStateIdentifier

& ValuedStateIdentifier> + ? TargetSelection, S, Owner>, P, "state", "branch"> : + never + : TargetSelection, P, P extends ChoiceIdentifier ? "choice" : "state", "branch"> + : never + : D extends { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: infer Ref + } ? RefPath extends infer P extends string ? BranchBuilderAt extends { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: infer B + } ? TargetSelection + : never : + never + : D extends { + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history: infer Ref + } ? + RefPath extends infer P extends string + ? TargetSelection>, P>, P, "history", "full"> : + never + : D extends { + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: infer Ref + } ? + RefPath extends infer P extends Extract, ValuedStateIdentifier> + ? TargetSelection, P, "update", "branch"> : + never + : D extends { + /** Accepts the event without selecting a new destination. */ + readonly none: true + } ? TargetSelection<() => NoTarget, never, "none", "local"> + : never + type BuilderArgs = K extends keyof B + ? B[K] extends (...args: infer A) => unknown ? A : never + : never + type ObjectConstruction = { + readonly [M in Extract]: BuilderArgs extends readonly [unknown?] ? + & { readonly [K in M]: (context: C) => BuilderArgs[0] } + & { readonly [K in Exclude<"from" | "decoded", M>]?: never } + : never + }[Extract] + type ObjectDefault = B extends { + /** Constructs schema make input from the typed source context. */ + readonly from: () => unknown + } | (() => unknown) ? { + /** Constructs schema make input from the typed source context. */ + readonly from?: never /** Supplies an already decoded schema value from the typed source context. */ + readonly decoded?: never + } + : never + type ObjectPolicy = { + /** Set to true to exit and reenter the handler source. */ + readonly reenter?: Reenter extends true ? boolean : never + /** Declines the transition before construction when the predicate returns false. */ + readonly guard?: "declinable" extends Acceptance ? ((context: C) => boolean) | undefined : never + } + type InlineDestination, C> = { + readonly [P in DestinationPath]: + & { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: ReferenceAt + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: never + } + & (ObjectConstruction> | ObjectDefault>) + }[DestinationPath] + type InlineCombined, C> = Src extends ChoiceIdentifier ? + never : + { + readonly [P in DestinationPath]: { + readonly [Owner in Extract, ParentStateIdentifier

& ValuedStateIdentifier>]: + & { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: ReferenceAt + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: ReferenceAt + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: never + } + & { + readonly [ + M in Extract & keyof StateUpdateBuilder, "from" | "decoded"> + ]: BuilderArgs, M> extends readonly [unknown?] ? + & { + readonly [K in M]: ( + context: C + ) => { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: BuilderArgs, M>[0] + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: BuilderArgs, M>[0] + } + } + & { readonly [K in Exclude<"from" | "decoded", M>]?: never } + : never + }[Extract & keyof StateUpdateBuilder, "from" | "decoded">] + }[Extract, ParentStateIdentifier

& ValuedStateIdentifier>] + }[DestinationPath] + type InlineUpdate, C> = Src extends ChoiceIdentifier ? + never : + { + readonly [P in Extract, ValuedStateIdentifier>]: + & { + /** Set to true to exit and reenter the handler source. */ + readonly reenter?: P extends Src ? never : boolean + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: ReferenceAt + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: never + } + & ObjectConstruction> + }[Extract, ValuedStateIdentifier>] + type InlineInitial, C> = { + readonly [P in Exclude, "">]: BranchBuilderAt extends { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: infer B + } ? + & { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: ReferenceAt + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: never + } + & (ObjectConstruction | ObjectDefault) : + never + }[Exclude, "">] + type BranchSelections, Group> = { + readonly [K in Extract]: { + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target: SelectionOf + } + } + type ObjectBranchResolver< + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + C, + B extends Readonly>, + Declinable extends boolean + > = ( + context: TransitionBranchesResolveContext & DeclineCapability, + enqueue: Enqueue, EmittedEventOf> + ) => BranchSelectionResult | (Declinable extends true ? Declined : never) + type InvalidBranchSelection, Group> = { + readonly [K in keyof Group]: Src extends ChoiceIdentifier ? Group[K] extends { + /** Accepts the event without selecting a new destination. */ + readonly none: true + } | { + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update: unknown + } ? K + : [SelectionBuilder>] extends [never] ? K + : never + : [SelectionBuilder>] extends [never] ? K + : never + }[keyof Group] + type ObjectBranches< + S extends StateSchemas, + Src extends StateNodeIdentifier, + C, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + R, + Acceptance extends TransitionAcceptance + > = { + readonly [K in Extract, string>]: + & ([InvalidBranchSelection[K]>] extends [never] ? unknown : never) + & { + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches: K + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Constructs schema make input from the typed source context. */ + readonly from?: never + /** Supplies an already decoded schema value from the typed source context. */ + readonly decoded?: never + } + & { + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: "declinable" extends Acceptance ? boolean : false + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve: ObjectBranchResolver[K]>, true> + } + }[Extract, string>] + /** + * A transition with an inspectable destination and source-specific construction. + * + * @category models + * @since 0.34.0 + */ + export type Transition< + S extends StateSchemas, + Src extends StateNodeIdentifier, + C, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + R, + Reenter extends boolean = false, + Acceptance extends TransitionAcceptance = "required" + > = + & ObjectPolicy + & ( + | InlineDestination + | InlineUpdate + | InlineCombined + | InlineInitial + | { + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history: ReferenceUnion> + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Accepts the event without selecting a new destination. */ + readonly none?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: never + /** Constructs schema make input from the typed source context. */ + readonly from?: never + /** Supplies an already decoded schema value from the typed source context. */ + readonly decoded?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: never + } + | (Src extends ChoiceIdentifier ? never : { + /** Accepts the event without selecting a new destination. */ + readonly none: true + /** Selects the declared descendant destination. Supply required values with from or decoded. */ + readonly target?: never + /** Replaces the complete value of a retained source or ancestor without replacing its active children. */ + readonly update?: never + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: never + /** Constructs schema make input from the typed source context. */ + readonly from?: never + /** Supplies an already decoded schema value from the typed source context. */ + readonly decoded?: never + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable?: "declinable" extends Acceptance ? boolean : false + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve?: ( + context: C & DeclineCapability, + enqueue: Enqueue, EmittedEventOf> + ) => undefined | Declined + }) + | ObjectBranches + ) + type SourceValue = { + readonly [Kind in "effects" | "streams" | "timers" | "logic" | "children"]: K extends keyof Registered + ? Registered[K] + : never + }["effects" | "streams" | "timers" | "logic" | "children"] + type SourceKind = { + readonly [Kind in "effects" | "streams" | "timers" | "logic" | "children"]: K extends keyof Registered + ? Kind : never - } - - // Compute every accumulated channel from one normalized node union instead - // of repeating recursive walks for states, errors, services, and outputs. - type HandlerTreeEvidence = StateNodeIdentifier extends - infer StateId extends StateNodeIdentifier ? - StateId extends StateNodeIdentifier ? HandlerNodeEvidence - : never - : never - - type HandleTreeResult< - AllStates extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Input extends Schema.Top, - UnhandledStates extends StateIdentifier, - E, + }["effects" | "streams" | "timers" | "logic" | "children"] + type SourceKeys = { + readonly [Kind in "effects" | "streams" | "timers" | "logic" | "children"]: keyof Registered + }["effects" | "streams" | "timers" | "logic" | "children"] + type SourceInput = F extends LazyProgramValue ? { readonly input?: never } : + F extends (...args: never[]) => unknown ? Parameters["length"] extends 1 ? { + /** Maps the owning state context to the registered source input. */ + readonly input: (context: C) => Parameters[0] + } : + never + : { + /** Maps the owning state context to the registered source input. */ + readonly input?: never + } + type ResolvedInvoke = SourceKind extends "effects" ? + { readonly effect: () => InvokeResolvedSource> } + : SourceKind extends "streams" ? { readonly stream: () => InvokeResolvedSource> } + : SourceKind extends "timers" ? { readonly after: Duration.Input } + : SourceKind extends "logic" ? { + /** Logic values or functions of one required input; their Effect channels remain inferred. */ + readonly logic: InvokeResolvedSource> + } + : SourceKind extends "children" ? + & { readonly child: SourceValue } + & (SourceValue extends ChildMachine ? { + readonly [InvokeTypeId]: { + readonly error: Types.Covariant | ActionError>> + readonly initialError: Types.Covariant> + readonly requirements: Types.Covariant> + } + } : + never) : + never + type RegisteredInvocationInput = SourceKind extends "children" ? + SourceValue extends ChildMachine ? InputSchema extends typeof Schema.Void ? { + /** Maps the owning state context to the registered source input. */ + readonly input?: never + } + : { + /** Maps the owning state context to the registered source input. */ + readonly input: (context: C) => Input + } : + never + : SourceInput, C> + type ObjectOutcome< + S extends StateSchemas, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + Src extends StateIdentifier, + In extends ReadonlyArray, + Pa extends ReadonlyArray, R, - InitialE, - InitialR, - FinalStates extends StateIdentifier, - Output, - OutputStates extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray, - Config - > = Machine< - AllStates, - Events, - Input, - Exclude["stateId"]>, - E | HandlerTreeEvidence["error"], - ExcludeCompatibleRuntime< - R | HandlerTreeEvidence["services"], - EventOf, - EmittedEventOf - >, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates | Extract["outputState"], StateIdentifier>, - InputEvents, - ParentEvents - > - + V, + Channel extends "onDone" | "onFailure" | "onElement" | "onSnapshot" + > = [V] extends [never] ? {} : Channel extends "onDone" ? { + /** Handles successful invocation or state completion with its exact output type. */ + readonly onDone: Transition< + S, + Src, + InvokeDoneContext, + Ev, + Em, + R, + true, + TransitionAcceptance + > + } : + Channel extends "onFailure" ? { + /** Handles the typed failure of the invoked source; required exactly when that channel is reachable. */ + readonly onFailure: Transition< + S, + Src, + InvokeFailureContext, + Ev, + Em, + R, + true, + TransitionAcceptance + > + } : + Channel extends "onElement" ? { + /** Handles each Stream element; required when the element type is reachable. */ + readonly onElement: Transition< + S, + Src, + InvokeElementContext, + Ev, + Em, + R, + true, + TransitionAcceptance + > + } : + { + /** Optionally handles active snapshots of invoked logic or a child machine. */ + readonly onSnapshot: Transition< + S, + Src, + InvokeSnapshotContext, + Ev, + Em, + R, + true, + TransitionAcceptance + > + } + type RegisteredInvokeError = SourceKind extends "children" + ? SourceValue extends ChildMachine ? Error | ActionError> : never + : InvokeRuntimeError> + /** + * A registered source invocation with its required input and reachable outcomes. + * + * @category models + * @since 0.34.0 + */ + export type Invocation< + S extends StateSchemas, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + Src extends StateIdentifier, + In extends ReadonlyArray, + Pa extends ReadonlyArray, + R + > = { + readonly [K in Extract, string>]: Types.Simplify< + & { + /** Name of the source registered in make. */ + readonly src: K + } + & (SourceKind extends "children" ? { + /** Lifecycle identifier; Effects, Streams, and timers default to their registered source name. */ + readonly id?: + never /** Typed runtime address required for invoked logic; child descriptors carry their own address. */ + readonly address?: never + } + : SourceKind extends "logic" ? { + /** Lifecycle identifier; Effects, Streams, and timers default to their registered source name. */ + readonly id: InvokeLifecycleId + /** Typed runtime address required for invoked logic; child descriptors carry their own address. */ + readonly address: ChildAddress>>> + } + : { + /** Lifecycle identifier; Effects, Streams, and timers default to their registered source name. */ + readonly id?: + InvokeLifecycleId /** Typed runtime address required for invoked logic; child descriptors carry their own address. */ + readonly address?: never + }) + & (SourceKind extends "children" + ? SourceValue extends ChildMachine + ? M extends EnsureExecutable, UnhandledStates, OutputStates> ? unknown : never + : never + : unknown) + & RegisteredInvocationInput> + & ObjectOutcome>, "onDone"> + & ObjectOutcome, "onFailure"> + & ObjectOutcome< + S, + Ev, + Em, + Src, + In, + Pa, + R, + SourceKind extends "streams" + ? Stream.Success>, Stream.Stream>> : + never, + "onElement" + > + & (SourceKind extends "logic" | "children" ? Partial< + ObjectOutcome>>, "onSnapshot"> + > + : {}) + > + }[Extract, string>] + type ObjectLifecycle< + S extends StateSchemas, + Node, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + Src extends StateNodeIdentifier, + In extends ReadonlyArray, + Pa extends ReadonlyArray + > = Src extends StateIdentifier ? + & { + /** Runs synchronously on entry and may enqueue commands. */ + readonly entry?: ( + context: StateActionContext, + enqueue: Enqueue, EmittedEventOf> + ) => StateActionResult + } + & (Node extends { readonly type: "final" } ? + & { + /** Runs synchronously on exit and may enqueue commands. */ + readonly exit?: never /** Constructs required values for the declared initial child or parallel regions. */ + readonly initialize?: never + } + & OutputHandlerConfig> + : { + /** Runs synchronously on exit and may enqueue commands. */ + readonly exit?: ( + context: StateActionContext, + enqueue: Enqueue, EmittedEventOf> + ) => StateActionResult + /** Constructs required values for the declared initial child or parallel regions. */ + readonly initialize?: StateInitializeHandler + } & ActiveOutputHandlerConfig) + : { + /** Runs synchronously on entry and may enqueue commands. */ + readonly entry?: never /** Runs synchronously on exit and may enqueue commands. */ + readonly exit?: never /** Constructs required values for the declared initial child or parallel regions. */ + readonly initialize?: never /** Constructs the decoded output declared by the state schema. */ + readonly output?: never + } + /** + * Handlers for a node of the declared root tree. + * + * @category models + * @since 0.34.0 + */ + export type StateHandler< + S extends StateSchemas, + Node, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + Src extends StateNodeIdentifier, + In extends ReadonlyArray, + Pa extends ReadonlyArray, + R + > = + & ObjectLifecycle + & (Src extends ChoiceIdentifier ? { + /** Required total transition for a transient choice state. */ + readonly choice: Transition, Ev, Em, R> + /** Event handlers keyed by the public or internal event tag. */ + readonly on?: never + /** Eventless transition evaluated during stabilization. */ + readonly always?: never + /** Handles successful invocation or state completion with its exact output type. */ + readonly onDone?: never + /** One registered invocation or an array with distinct lifecycle identities. */ + readonly invoke?: never + /** Child handlers nested according to the declared root topology. */ + readonly states?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + } + : Src extends StateIdentifier ? Node extends { readonly type: "final" } ? { + /** Event handlers keyed by the public or internal event tag. */ + readonly on?: never + /** Eventless transition evaluated during stabilization. */ + readonly always?: never + /** Handles successful invocation or state completion with its exact output type. */ + readonly onDone?: never + /** Required total transition for a transient choice state. */ + readonly choice?: never + /** One registered invocation or an array with distinct lifecycle identities. */ + readonly invoke?: never + /** Child handlers nested according to the declared root topology. */ + readonly states?: never + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + } + : + & { + /** Event handlers keyed by the public or internal event tag. */ + readonly on?: { + readonly [Tag in TagOf]?: Transition< + S, + Src, + HandlerContext, + Ev, + Em, + R, + true, + TransitionAcceptance + > + } + /** Eventless transition evaluated during stabilization. */ + readonly always?: Transition< + S, + Src, + AlwaysContext, + Ev, + Em, + R, + false, + TransitionAcceptance + > + /** Handles successful invocation or state completion with its exact output type. */ + readonly onDone?: Transition< + S, + Src, + DoneContext, + Ev, + Em, + R, + false, + TransitionAcceptance + > + /** Required total transition for a transient choice state. */ + readonly choice?: never + /** One registered invocation or an array with distinct lifecycle identities. */ + readonly invoke?: + | Invocation + | (ReadonlyArray> & { + /** Name of the source registered in make. */ + readonly src?: never + }) + } + & (Node extends { + /** Child handlers nested according to the declared root topology. */ + readonly states: infer Children extends StateSchemas + } ? { + /** Child handlers nested according to the declared root topology. */ + readonly states?: { + readonly [K in ActiveStateKey | ChoiceStateKey]?: StateHandler< + S, + Children[K], + Ev, + Em, + Extract, StateNodeIdentifier>, + In, + Pa, + R + > + } + /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: HistoryDefaultConfig + } : + { + /** Child handlers nested according to the declared root topology. */ + readonly states?: never /** Restores the declared history reference, using its fallback on first entry. */ + readonly history?: never + }) : + never) + type ValidateObjectTransition = + & { + readonly [ + K in Exclude< + keyof T, + | "target" + | "update" + | "initial" + | "history" + | "none" + | "branches" + | "from" + | "decoded" + | "resolve" + | "guard" + | "reenter" + | "declinable" + > + ]: never + } + & (T extends { + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable: infer D + } ? boolean extends D ? { + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable: never + } : + unknown + : unknown) + & (T extends { + /** Set to true when the resolver can explicitly return decline(). */ + readonly declinable: true + } ? unknown + : T extends { + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve: (...args: any[]) => infer Result + } ? [Extract] extends [never] ? unknown : { + /** Selects one declared branch and may enqueue synchronous commands. */ + readonly resolve: never + } + : unknown) + type AllowedInvokeChannels = + | ([InvokeOutput>] extends [never] ? never : "onDone") + | ([RegisteredInvokeError] extends [never] ? never : "onFailure") + | (SourceKind extends "streams" + ? [Stream.Success>, Stream.Stream>>] extends [never] + ? never + : "onElement" + : never) + | (SourceKind extends "logic" | "children" ? "onSnapshot" : never) + type ValidateObjectInvoke = I extends ReadonlyArray ? + { readonly [K in keyof I]: ValidateObjectInvoke } : + I extends { + /** Name of the source registered in make. */ + readonly src: infer Src extends PropertyKey + } ? { + readonly [K in keyof I]: K extends AllowedInvokeChannels ? ValidateObjectTransition + : K extends "src" | "id" | "address" | "input" ? unknown + : never + } : + never + type ObjectDeclarations = + | Extract + | (C extends { readonly states: infer Children } + ? { readonly [K in keyof Children]: ObjectDeclarations }[keyof Children] + : never) + type ValidateObjectConfig = [ObjectDeclarations] extends [never] ? unknown : { + readonly [K in keyof C]: K extends "states" ? { readonly [P in keyof C[K]]: ValidateObjectConfig } + : K extends "on" ? { readonly [P in keyof C[K]]: ValidateObjectTransition } + : K extends "always" | "onDone" | "choice" ? ValidateObjectTransition + : K extends "invoke" ? ValidateObjectInvoke + : unknown + } + type NormalizeObjectInvoke = I extends ReadonlyArray ? + { readonly [K in keyof I]: NormalizeObjectInvoke } + : I extends { + /** Name of the source registered in make. */ + readonly src: infer K extends PropertyKey + } ? Omit & ResolvedInvoke + : I + type NormalizeObjectConfig = [SourceKeys] extends [never] ? C : { + readonly [K in keyof C]: K extends "states" ? { readonly [P in keyof C[K]]: NormalizeObjectConfig } + : K extends "invoke" ? NormalizeObjectInvoke + : C[K] + } /** - * Adds state handlers from a root state object. + * Adds object handlers while checking topology, protocols, invocation outcomes, and readiness. * * @category combinators - * @since 0.4.0 + * @since 0.34.0 */ export interface Handler< - States extends StateSchemas, - Events extends ReadonlyArray, - Emits extends ReadonlyArray, - Input extends Schema.Top, - UnhandledStates extends StateIdentifier, - E, - R, - InitialE, - InitialR, - FinalStates extends StateIdentifier, - Output, - OutputStates extends StateIdentifier, - InputEvents extends ReadonlyArray, - ParentEvents extends ReadonlyArray + S extends StateSchemas, + Ev extends ReadonlyArray, + Em extends ReadonlyArray, + I extends Schema.Top, + IE, + IR, + Final extends StateIdentifier, + Out, + In extends ReadonlyArray, + Pa extends ReadonlyArray, + R > { - < - const Config - >( + ( config: - & Config - & (Config extends (...args: never[]) => unknown ? never : unknown) - & HandlerNode< - States, - States[""], - Events, - Emits, - E, - R, - InputEvents, - ParentEvents, - Extract<"", StateNodeIdentifier> - > + & C + & (C extends (...args: never[]) => unknown ? never : unknown) + & StateHandler>, In, Pa, R> + & ValidateObjectConfig, R> & RootHandlerValidation< - States, - Events, - InputEvents, - Emits, - NoInfer, - | OutputStates - | Extract< - HandlerTreeEvidence }>["outputState"], - StateIdentifier + S, + Ev, + In, + Em, + NormalizeObjectConfig, R>, + Extract< + HandlerTreeEvidence, R> }>["outputState"], + StateIdentifier > > ): HandleTreeResult< - States, - Events, - Emits, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - OutputStates, - InputEvents, - ParentEvents, - { readonly "": Config } + S, + Ev, + Em, + I, + StateIdentifier, + never, + never, + IE, + IR, + Final, + Out, + never, + In, + Pa, + { readonly "": NormalizeObjectConfig } > } @@ -8768,15 +7689,19 @@ export declare namespace Machine { E, R > { + /** Runs synchronously on entry and may enqueue commands. */ readonly entry?: ( context: StateActionContext, enqueue: Enqueue, EmittedEventOf> ) => StateActionResult + /** Runs synchronously on exit and may enqueue commands. */ readonly exit?: ( context: StateActionContext, enqueue: Enqueue, EmittedEventOf> ) => StateActionResult + /** One registered invocation or an array with distinct lifecycle identities. */ readonly invoke?: StoredInvokeDefinition + /** Eventless transition evaluated during stabilization. */ readonly always?: TransitionConfig< States, Events, @@ -8786,6 +7711,7 @@ export declare namespace Machine { false, TransitionAcceptance > + /** Handles successful invocation or state completion with its exact output type. */ readonly onDone?: TransitionConfig< States, Events, @@ -8795,9 +7721,11 @@ export declare namespace Machine { false, TransitionAcceptance > + /** Constructs the decoded output declared by the state schema. */ readonly output?: | ((context: FinalOutputContext) => unknown) | ((context: ParallelOutputContext) => unknown) + /** Event handlers keyed by the public or internal event tag. */ readonly on?: EventHandlerMap } @@ -8849,7 +7777,10 @@ export declare namespace Snapshot { export type Node = State extends { readonly path: string; readonly value: unknown } ? | State | (State extends { readonly state: infer Child } ? Node - : State extends { readonly states: infer Regions } ? Node + : State extends { + /** Child handlers nested according to the declared root topology. */ + readonly states: infer Regions + } ? Node : never) : never /** Every valid absolute path in a logical snapshot type. */ @@ -8977,6 +7908,76 @@ export const isFinal: < */ export const state: StateConstructor = internal.state as StateConstructor +/** + * An opaque reference to one node in a declared state tree. + * + * @category models + * @since 0.34.0 + */ +export interface TargetReference< + Root extends Machine.StateNodeConfig, + Path extends string, + Kind extends "state" | "choice" | "history" +> { + readonly [internal.TargetReferenceTypeId]: { + readonly root: { readonly node: Root } + readonly path: Path + readonly kind: Kind + } +} + +type TargetReferenceTree = + & TargetReference< + Root, + Path, + Node extends { readonly type: "history" } ? "history" + : Node extends { readonly type: "choice" } ? "choice" + : "state" + > + & (Node extends { + /** Child handlers nested according to the declared root topology. */ + readonly states: infer Children + } ? { + readonly [Key in Extract]: TargetReferenceTree< + Root, + Children[Key], + Machine.JoinPath + > + } : + {}) + +/** + * References for every path in a declared root tree. + * + * @category models + * @since 0.34.0 + */ +export interface Targets { + /** Root data owner and the starting point for descendant state references. */ + readonly root: TargetReferenceTree +} + +/** + * Derives immutable state references from a root descriptor. + * + * References follow the declared tree: `targets.root.Checkout.Review`. + * Creating references does not construct state values or start machine work. + * + * @category constructors + * @since 0.34.0 + */ +export const targets: (root: State) => Targets = internal + .targets as unknown as ( + root: State + ) => Targets + +type UniqueSourceNames = [ + | Extract + | Extract + | Extract + | Extract +] extends [never] ? unknown : { readonly "~effect/Machine/DuplicateSourceName": never } + type MakeConfig< Root extends Machine.StateNodeConfig, InputEvents extends ReadonlyArray, @@ -8985,8 +7986,28 @@ type MakeConfig< InitialE, InitialR, InternalEvents extends ReadonlyArray, - ParentDeclaration extends Parent.Any | undefined + ParentDeclaration extends Parent.Any | undefined, + Effects extends Machine.EffectSources, + Streams extends Machine.StreamSources, + Timers extends Machine.TimerSources, + Logics extends Machine.LogicSources, + Children extends Machine.ChildSources, + Branches extends Machine.BranchDefinitions > = { + /** Lazy Effects or functions with one required input, invoked by registered name. */ + readonly effects?: Effects & Machine.ValidatePrograms + /** Lazy Streams or functions with one required input, invoked by registered name. */ + readonly streams?: Streams & Machine.ValidatePrograms + /** Cancellable delays, supplied as durations or functions of one required input. */ + readonly timers?: Timers & Machine.ValidatePrograms + /** Logic values or functions of one required input; their Effect channels remain inferred. */ + readonly logic?: Logics & Machine.ValidatePrograms & Machine.ValidateLogicSources> + /** Child machine descriptors registered for state-owned invocation. */ + readonly children?: Children + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches?: + & Branches + & { readonly [K in keyof Branches]: ValidateTransitionBranchRecord> } /** Stable definition identifier used by inspection and visualization. */ readonly id?: string /** State topology and value schemas, captured by a `Machine.state` descriptor. */ @@ -9018,7 +8039,13 @@ type MakeResult< InitialE, InitialR, InternalEvents extends ReadonlyArray, - ParentDeclaration extends Parent.Any | undefined + ParentDeclaration extends Parent.Any | undefined, + Effects extends Machine.EffectSources, + Streams extends Machine.StreamSources, + Timers extends Machine.TimerSources, + Logics extends Machine.LogicSources, + Children extends Machine.ChildSources, + Branches extends Readonly>>> > = Definition< { readonly "": Root }, readonly [...InputEvents, ...InternalEvents], @@ -9029,14 +8056,37 @@ type MakeResult< Machine.TerminalOutput<{ readonly "": Root }>, Emits, InputEvents, - Machine.ParentEventsOf + Machine.ParentEventsOf, + { + /** Lazy Effects or functions with one required input, invoked by registered name. */ + readonly effects: Effects + /** Lazy Streams or functions with one required input, invoked by registered name. */ + readonly streams: Streams + /** Cancellable delays, supplied as durations or functions of one required input. */ + readonly timers: Timers + /** Logic values or functions of one required input; their Effect channels remain inferred. */ + readonly logic: Logics + /** Child machine descriptors registered for state-owned invocation. */ + readonly children: Children + /** Named groups of inspectable destinations used by transition resolvers. */ + readonly branches: Branches + } > type RootInitialization = & { readonly initialConfiguration?: never } - & (Machine.NodeSchema extends never ? { readonly initial?: Machine.RootInitialBuilderInput } - : {} extends NodeMakeInput ? { readonly initial?: Machine.RootInitialBuilderInput } - : { readonly initial: Machine.RootInitialBuilderInput }) + & (Machine.NodeSchema extends never ? { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: Machine.RootInitialBuilderInput + } + : {} extends NodeMakeInput ? { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial?: Machine.RootInitialBuilderInput + } + : { + /** Enters a compound or parallel subtree through its declared initialization. */ + readonly initial: Machine.RootInitialBuilderInput + }) /** @inline */ interface Make { @@ -9049,10 +8099,32 @@ interface Make { InitialE = never, InitialR = never, const InternalEvents extends ReadonlyArray = readonly [], - const ParentDeclaration extends Parent.Any | undefined = undefined + const ParentDeclaration extends Parent.Any | undefined = undefined, + const Effects extends Machine.EffectSources = {}, + const Streams extends Machine.StreamSources = {}, + const Timers extends Machine.TimerSources = {}, + const Logics extends Machine.LogicSources = {}, + const Children extends Machine.ChildSources = {}, + const Branches extends Machine.BranchDefinitions> = {} >( config: - & MakeConfig + & MakeConfig< + Root, + InputEvents, + Emits, + Input, + InitialE, + InitialR, + InternalEvents, + ParentDeclaration, + Effects, + Streams, + Timers, + Logics, + Children, + Branches + > + & UniqueSourceNames, NoInfer, NoInfer, NoInfer, NoInfer> & { /** Mutually exclusive with a complete initial configuration. */ readonly initial?: never @@ -9070,7 +8142,13 @@ interface Make { InitialE, InitialR, InternalEvents, - ParentDeclaration + ParentDeclaration, + Effects, + Streams, + Timers, + Logics, + Children, + Branches > /** @param config Complete schema-first machine definition. */ < @@ -9081,10 +8159,32 @@ interface Make { InitialE = never, InitialR = never, const InternalEvents extends ReadonlyArray = readonly [], - const ParentDeclaration extends Parent.Any | undefined = undefined + const ParentDeclaration extends Parent.Any | undefined = undefined, + const Effects extends Machine.EffectSources = {}, + const Streams extends Machine.StreamSources = {}, + const Timers extends Machine.TimerSources = {}, + const Logics extends Machine.LogicSources = {}, + const Children extends Machine.ChildSources = {}, + const Branches extends Machine.BranchDefinitions> = {} >( config: - & MakeConfig + & MakeConfig< + Root, + InputEvents, + Emits, + Input, + InitialE, + InitialR, + InternalEvents, + ParentDeclaration, + Effects, + Streams, + Timers, + Logics, + Children, + Branches + > + & UniqueSourceNames, NoInfer, NoInfer, NoInfer, NoInfer> & Exclude< RootInitialization, Input["Type"]>, { readonly initialConfiguration: unknown } @@ -9097,7 +8197,13 @@ interface Make { InitialE, InitialR, InternalEvents, - ParentDeclaration + ParentDeclaration, + Effects, + Streams, + Timers, + Logics, + Children, + Branches > } @@ -9142,6 +8248,7 @@ interface Make { * * const States = Machine.state({ initial: "Count", states: { Count } }) * const Events = Machine.eventsFromSchemas(Increment) + * const targets = Machine.targets(States) * * const counter = Machine.make({ * root: States, @@ -9150,9 +8257,10 @@ interface Make { * }).handle({ states: { * Count: { * on: { - * Increment: (to) => - * to.branch.Count().resolve(({ event, state, target }) => - * target.decoded(new Count({ value: state.value + event.by }))) + * Increment: { + * update: targets.root.Count, + * decoded: ({ event, state }) => new Count({ value: state.value + event.by }) + * } * } * } * } }) @@ -9524,25 +8632,24 @@ type TransitionBranchRecordError = Extract - -type InvalidUpdatingTransitionBranchKey = { - readonly [Key in keyof Branches]: Branches[Key] extends { - readonly target: { readonly "~effect/Machine/UpdatingTransitionTarget": string } - } ? Key - : never -}[keyof Branches] +type InvalidStaticTransitionBranchKey = Extract< + keyof Branches, + "" | number | symbol | `${number}` | "NaN" | "Infinity" | "-Infinity" +> type ValidateTransitionBranchRecord = [keyof Branches] extends [never] ? TransitionBranchRecordError<"Branch records must contain at least one branch"> - : [InvalidStaticTransitionBranchKey] extends [never] ? - [InvalidUpdatingTransitionBranchKey] extends [never] ? unknown - : TransitionBranchRecordError< - "Updating targets require a direct resolver", - InvalidUpdatingTransitionBranchKey - > - : TransitionBranchRecordError< - "Branch keys must be non-empty, non-index strings", + : [InvalidStaticTransitionBranchKey] extends [never] ? { + readonly [K in keyof Branches]: + & { + readonly [ + Extra in Exclude + ]: never + } + & (Branches[K] extends { readonly title: "" } ? { readonly title: never } : unknown) + } : + TransitionBranchRecordError< + "Branch keys must be non-empty, non-numeric strings", InvalidStaticTransitionBranchKey > @@ -9650,10 +8757,12 @@ export const planInitial: < & ( | { readonly done: true + /** Constructs the decoded output declared by the state schema. */ readonly output: Output } | { readonly done: false + /** Constructs the decoded output declared by the state schema. */ readonly output: undefined } ), @@ -9859,7 +8968,7 @@ export const enabled: < * events: Machine.eventsFromSchemas(), * internalEvents, * initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.from())) - * }).handle({ states: { Idle: { on: { Loaded: (to) => to.none } } } }) + * }).handle({ states: { Idle: { on: { Loaded: { none: true } } } } }) * * export const canLoad = Effect.gen(function*() { * const initial = yield* Machine.planInitial(machine) @@ -9977,6 +9086,7 @@ export const can: { * class On extends Schema.TaggedClass("On")("On", {}) {} * class Toggle extends Schema.TaggedClass("Toggle")("Toggle", {}) {} * const States = Machine.state({ initial: "Off", states: { Off, On } }) + * const targets = Machine.targets(States) * const machine = Machine.make({ * root: States, * events: Machine.eventsFromSchemas(Toggle), @@ -9984,8 +9094,7 @@ export const can: { * }).handle({ states: { * Off: { * on: { - * Toggle: (to) => - * to.branch.On().resolve(({ target }) => target.from()) + * Toggle: { target: targets.root.On } * } * }, * On: {} @@ -10066,10 +9175,12 @@ export const plan: < & ( | { readonly done: true + /** Constructs the decoded output declared by the state schema. */ readonly output: Output } | { readonly done: false + /** Constructs the decoded output declared by the state schema. */ readonly output: undefined } ), @@ -10114,6 +9225,7 @@ export const logic: < InitialError = never, InitialRequirements = never >(options: { + /** Enters a compound or parallel subtree through its declared initialization. */ readonly initial: | State | (( @@ -10172,7 +9284,7 @@ export const childAddress: (id: string) => ChildAddress = * This Effect requires a managed process runtime. A named child id must be * unique for the current parent until that child stops. * - * Use a state's `invoke: (from) => from.logic(...)` declaration for children + * Register logic in `make({ logic })` and use `invoke: { src, id, address, ...outcomes }` for children * that start and stop with that state. * @see {@link sendTo} for sending events to named children. * @category runtime diff --git a/packages/effect-machine/src/internal/machine/declaration.ts b/packages/effect-machine/src/internal/machine/declaration.ts new file mode 100644 index 00000000..f3d27233 --- /dev/null +++ b/packages/effect-machine/src/internal/machine/declaration.ts @@ -0,0 +1,140 @@ +/** Captures reusable sources and topology declarations before handler compilation. */ +import * as Effect from "effect/Effect" +import { hasProperty } from "effect/Predicate" +import * as Stream from "effect/Stream" +import type { Machine, State } from "../../Machine.js" +import * as Reference from "./targetReference.js" +import * as Topology from "./topology.js" + +type SourceKind = "effects" | "streams" | "timers" | "logic" | "children" +interface Source { + readonly kind: SourceKind + readonly value: unknown +} +export interface Declaration { + readonly root: State + readonly sources: ReadonlyMap + readonly branches: ReadonlyMap>>>> +} +const record = (value: unknown, message: string): Record => { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(message) + return value as Record +} +export const capture = ( + root: State, + config: Readonly> +): Declaration => { + const sources = new Map() + for (const kind of ["effects", "streams", "timers", "logic", "children"] as const) { + if (config[kind] === undefined) continue + for (const [name, value] of Object.entries(record(config[kind], `Machine ${kind} must be a source map`))) { + if (sources.has(name)) throw new Error(`Machine source name "${name}" is registered more than once`) + sources.set(name, Object.freeze({ kind, value })) + } + } + const branches = new Map>>>>() + if (config.branches !== undefined) { + for (const [name, group] of Object.entries(record(config.branches, "Machine branches must be a map"))) { + const branchRecord = record(group, `Machine branches "${name}" must be a branch record`) + for (const key of Reflect.ownKeys(branchRecord)) { + if (typeof key !== "string") throw new Error("Machine branches cannot use symbol keys") + if (key.length === 0 || String(Number(key)) === key) { + throw new Error("Machine branches require non-index string branch keys") + } + } + const entries = Object.entries(branchRecord) + if (entries.length === 0) throw new Error(`Machine branches "${name}" requires a branch`) + branches.set( + name, + Object.freeze(Object.fromEntries(entries.map(([key, value]) => { + const spec = { ...record(value, `Machine branch "${name}.${key}" must be a declaration`) } + for (const field of Reflect.ownKeys(spec)) { + if ( + typeof field !== "string" || !["target", "update", "initial", "history", "none", "title"].includes(field) + ) { + throw new Error(`Machine branch "${name}.${key}" contains an unknown declaration field`) + } + } + if (spec.title !== undefined && (typeof spec.title !== "string" || spec.title.length === 0)) { + throw new Error("Machine branch title must be a non-empty string") + } + selection({ root, sources, branches }, spec) + return [key, Object.freeze(spec)] + }))) + ) + } + } + return { root, sources, branches } +} +const reference = (value: unknown, root: Declaration["root"]): Reference.Reference[typeof Reference.TypeId] => { + if (!hasProperty(value, Reference.TypeId)) throw new Error("Machine target must be a declared state reference") + const ref = (value as Reference.Reference)[Reference.TypeId] + if (ref.root !== root) throw new Error("Machine target reference belongs to a different root descriptor") + return ref +} +export const selection = ( + declaration: Declaration, + config: Readonly> +): Topology.TargetSelection => { + const keys = ["target", "initial", "history", "none"].filter((key) => config[key] !== undefined) + if (keys.length === 0 && config.update !== undefined) { + const owner = reference(config.update, declaration.root) + if (owner.kind !== "state") throw new Error("Machine update requires an active state reference") + return Topology.makeTargetSelection("update", owner.path, "branch") + } + if (keys.length !== 1) throw new Error("Machine transition must declare exactly one destination operation") + const key = keys[0]! + if (key === "none") { + if (config.none !== true || config.update !== undefined) { + throw new Error("Machine targetless transition requires none: true") + } + return Topology.noneTargetSelection + } + const ref = reference(config[key], declaration.root) + if (ref.path === "") throw new Error("Machine root data changes use update; transitions select a descendant") + if ((key === "history") !== (ref.kind === "history")) { + throw new Error("Machine history references require a history transition") + } + if (key === "initial" && ref.kind !== "state") { + throw new Error("Machine initial entry requires an active state reference") + } + const update = config.update === undefined ? undefined : reference(config.update, declaration.root).path + if (update !== undefined && key !== "target") throw new Error("Machine owner updates require an ordinary destination") + return Topology.makeTargetSelection( + key === "initial" ? "initial" : ref.kind, + ref.path, + key === "history" ? "full" : "branch", + update + ) +} +export const invocation = ( + declaration: Declaration, + value: unknown, + path: string, + adaptContext: (context: unknown) => unknown +): Record => { + const config = record(value, `Machine invocation for "${path}" must be an object`) + const source = typeof config.src === "string" ? declaration.sources.get(config.src) : undefined + if (source === undefined) throw new Error(`Machine invocation for "${path}" names an unknown source`) + const { src, input: rawInput, ...rest } = config + const input = typeof rawInput === "function" ? (context: unknown) => rawInput(adaptContext(context)) : rawInput + if (source.kind === "children") return { ...rest, child: source.value, ...(input === undefined ? {} : { input }) } + // Service classes are functions at runtime and also lazy Effect values. + // Recognize Effect/Stream protocols before treating a function as an input adapter. + const parameterized = typeof source.value === "function" && + !(source.kind === "effects" && Effect.isEffect(source.value)) && + !(source.kind === "streams" && Stream.isStream(source.value)) + if (parameterized !== (typeof input === "function")) { + throw new Error(`Machine invocation "${src}" requires input exactly when its source takes an argument`) + } + const resolve = parameterized + ? (context: unknown) => + (source.value as (input: unknown) => unknown)((input as (context: unknown) => unknown)(context)) + : () => source.value + const field = { effects: "effect", streams: "stream", timers: "after", logic: "logic" }[source.kind] + return { + ...rest, + id: rest.id ?? src, + [field]: !parameterized && (source.kind === "timers" || source.kind === "logic") ? source.value : resolve + } +} diff --git a/packages/effect-machine/src/internal/machine/invocation.ts b/packages/effect-machine/src/internal/machine/invocation.ts index 933c48d7..2a39eb61 100644 --- a/packages/effect-machine/src/internal/machine/invocation.ts +++ b/packages/effect-machine/src/internal/machine/invocation.ts @@ -61,7 +61,7 @@ const streamLogic = ( } }) -const resolveValue = (value: unknown, context: Machine.InvokeContext): unknown => +const resolveValue = (value: unknown, context: Omit, "root">): unknown => typeof value === "function" ? value(context) : value const makeChildOwner = (scope: Runtime.ProcessScope): ChildOwner => ({ @@ -76,7 +76,7 @@ const makeChildOwner = (scope: Runtime.ProcessScope): ChildOwner => ({ const resolveOne = ( raw: InvocationDefinition.InvocationDefinition, - context: Machine.InvokeContext, + context: Omit, "root">, path: string ): AnyConfig => { if ("effect" in raw) { diff --git a/packages/effect-machine/src/internal/machine/invocationDefinition.ts b/packages/effect-machine/src/internal/machine/invocationDefinition.ts index 3e74e382..6992f020 100644 --- a/packages/effect-machine/src/internal/machine/invocationDefinition.ts +++ b/packages/effect-machine/src/internal/machine/invocationDefinition.ts @@ -6,7 +6,7 @@ import type { EventTransition } from "./transition.js" // Source callbacks cross the erased machine-definition boundary here. Their // state, event, error and service relationships are checked by the public builder. -type Context = Machine.InvokeContext +type Context = Omit, "root"> interface Outcomes { readonly onDone?: EventTransition readonly onFailure?: EventTransition diff --git a/packages/effect-machine/src/internal/machine/machine.ts b/packages/effect-machine/src/internal/machine/machine.ts index ffaf5b26..6c6ac7ce 100644 --- a/packages/effect-machine/src/internal/machine/machine.ts +++ b/packages/effect-machine/src/internal/machine/machine.ts @@ -28,6 +28,7 @@ import type { } from "../../Machine.js" import * as Activities from "./activities.js" import * as Configuration from "./configuration.js" +import * as Declaration from "./declaration.js" import type { ChildAlreadyExistsError, InfiniteTransitionError, StartupError } from "./errors.js" import type { CapturedStateConfig } from "./implementation.js" import * as InvocationDefinition from "./invocationDefinition.js" @@ -47,6 +48,7 @@ import { makeTargetBuilder, withFrom } from "./targetBuilder.js" +import * as TargetReference from "./targetReference.js" import * as Topology from "./topology.js" export { @@ -60,11 +62,14 @@ export { } from "./errors.js" export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId } from "./symbols.js" +/** Internal seam for the public target-reference factory and its opaque brand. */ +export const TargetReferenceTypeId: typeof TargetReference.TypeId = TargetReference.TypeId +export const targets: (root: State) => { readonly root: TargetReference.Reference } = + TargetReference.make + const TypeId = "~effect/Machine" const ParentTypeId = "~effect/Machine/Parent" export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke") -export const TransitionTypeId: unique symbol = Symbol.for("effect/Machine/Transition") -const InvokeBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/InvokeBuilderDescriptor") const ChildMachineTypeId = "~effect/Machine/ChildMachine" type MachineRuntimeRequirement = internalRuntime.MachineRuntime @@ -134,28 +139,6 @@ type CapturedNamedBranch = { readonly selection: Topology.TargetSelection } -const TransitionBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/TransitionBuilderDescriptor") - -type DirectTransitionDescriptor = { - readonly [TransitionBuilderDescriptorTypeId]: typeof TransitionBuilderDescriptorTypeId - readonly type: "direct" - readonly selection: Topology.TargetSelection - readonly resolver?: (context: any, enqueue: unknown) => unknown - readonly reenterSource: boolean - readonly declinable: boolean -} - -type BranchesTransitionDescriptor = { - readonly [TransitionBuilderDescriptorTypeId]: typeof TransitionBuilderDescriptorTypeId - readonly type: "branches" - readonly declarations: unknown - readonly resolve: (context: any, enqueue: unknown) => unknown - readonly reenterSource: boolean - readonly declinable: boolean -} - -type TransitionBuilderDescriptor = DirectTransitionDescriptor | BranchesTransitionDescriptor - const InitialBuilderDescriptorTypeId: unique symbol = Symbol("effect/Machine/InitialBuilderDescriptor") type InitialBuilderDescriptor = { @@ -169,34 +152,6 @@ const plainTargetSelection = (selection: Topology.TargetSelection): Topology.Tar ? Topology.noneTargetSelection : Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, selection.updatePath) -const transitionOptions = (options: unknown): { readonly declinable: boolean } => { - const configuration = typeof options === "object" && options !== null - ? options as { readonly declinable?: unknown } - : {} - if (hasProperty(configuration, "reenter")) { - throw new Error("Use .reenter() before construction instead of the resolver reenter option") - } - return { declinable: configuration.declinable === true } -} - -const makeDirectTransitionDescriptor = ( - selection: Topology.TargetSelection, - resolve: ((context: any, enqueue: unknown) => unknown) | undefined, - options: unknown, - reenterSource = false -): DirectTransitionDescriptor => { - const shared: Omit = { - [TransitionBuilderDescriptorTypeId]: TransitionBuilderDescriptorTypeId, - type: "direct", - selection: plainTargetSelection(selection), - reenterSource, - ...transitionOptions(options) - } - return resolve === undefined - ? Object.freeze(shared) - : Object.freeze({ ...shared, resolver: resolve }) -} - // The authored callback crosses an erased schema boundary here. Each builder // retains its construction mode, and planning validates both resulting values. const constructSelectionValue = ( @@ -211,73 +166,6 @@ const constructSelectionValue = ( return context.target[method](values.target).update(context.owner[method](values.update)) } -const guardedSelection = ( - selection: Topology.TargetSelection, - predicate: (context: any) => boolean, - reenterSource: boolean -): object => { - const wrap = (resolve: (context: any, enqueue: unknown) => unknown, options?: unknown) => - makeDirectTransitionDescriptor( - selection, - (context, enqueue) => predicate(context) ? resolve(context, enqueue) : Topology.makeDeclined(), - { ...transitionOptions(options), declinable: true }, - reenterSource - ) - return Object.freeze({ - ...wrap((context) => selection.kind === "none" ? undefined : constructSelectedTarget(context.target)), - from: (value: (context: any) => unknown) => - wrap((context) => constructSelectionValue(selection, context, "from", value(context))), - decoded: (value: (context: any) => unknown) => - wrap((context) => constructSelectionValue(selection, context, "decoded", value(context))), - resolve: wrap - }) -} - -const decorateTransitionSelection = ( - selection: Topology.TargetSelection, - reenterSource = false -): Topology.TargetSelection => - Object.freeze({ - ...selection, - ...(reenterSource ? makeDirectTransitionDescriptor(selection, undefined, undefined, true) : {}), - guard: (predicate: (context: any) => boolean) => guardedSelection(selection, predicate, reenterSource), - from: (value: (context: any) => unknown) => - makeDirectTransitionDescriptor( - selection, - (context) => constructSelectionValue(selection, context, "from", value(context)), - undefined, - reenterSource - ), - decoded: (value: (context: any) => unknown) => - makeDirectTransitionDescriptor( - selection, - (context) => constructSelectionValue(selection, context, "decoded", value(context)), - undefined, - reenterSource - ), - resolve: (resolve: (context: any, enqueue: unknown) => unknown, options?: unknown) => - makeDirectTransitionDescriptor(selection, resolve, options, reenterSource), - reenter: () => decorateTransitionSelection(selection, true), - updating: (owner: unknown) => { - if (typeof owner !== "function") { - throw new Error("Machine updating owner must be a state selector") - } - const ownerSelection = owner() - if ( - !Topology.isTargetSelection(ownerSelection) || ownerSelection.kind !== "state" || - ownerSelection.scope !== "branch" - ) { - throw new Error("Machine updating owner must be addressed by one branch state selector") - } - return decorateTransitionSelection( - Topology.makeTargetSelection(selection.kind, selection.path, selection.scope, ownerSelection.path), - reenterSource - ) - } - }) - -const noneTransitionSelection = decorateTransitionSelection(Topology.noneTargetSelection) - const makeInitialBuilderDescriptor = ( selection: Topology.TargetSelection, resolve: (context: any) => unknown @@ -319,89 +207,6 @@ const decorateInitialSelectorNode = (node: unknown): unknown => { return node } -const decorateTransitionSelectorNode = (node: unknown): unknown => { - if (Topology.isTargetSelection(node)) { - if (node.kind === "state" && node.scope === "full" && hasProperty(node, "initial")) { - return Object.freeze({ - ...decorateTransitionSelection(node), - initial: decorateTransitionSelection(node.initial as Topology.TargetSelection) - }) - } - return node === Topology.noneTargetSelection ? noneTransitionSelection : decorateTransitionSelection(node) - } - if (typeof node === "function") { - const wrapped = ((...args: ReadonlyArray) => decorateTransitionSelection(node(...args))) as - & ((...args: ReadonlyArray) => unknown) - & Record - for (const key of Object.keys(node)) { - wrapped[key] = decorateTransitionSelectorNode((node as unknown as Record)[key]) - } - return Object.freeze(wrapped) - } - if (typeof node === "object" && node !== null) { - const wrapped: Record = {} - for (const key of Object.keys(node)) { - wrapped[key] = decorateTransitionSelectorNode((node as Record)[key]) - } - return Object.freeze(wrapped) - } - return node -} - -const decorateBranches = (declarations: unknown, reenterSource = false): object => - Object.freeze({ - reenter: () => decorateBranches(declarations, true), - resolve: ( - resolve: (context: any, enqueue: unknown) => unknown, - options?: unknown - ): BranchesTransitionDescriptor => - Object.freeze({ - [TransitionBuilderDescriptorTypeId]: TransitionBuilderDescriptorTypeId, - type: "branches", - declarations, - resolve, - reenterSource, - ...transitionOptions(options) - }) - }) - -const makeTransitionSelector = ( - stateNodes: Machine.StateNodes, - source: string -): unknown => - Object.freeze({ - ...decorateTransitionSelectorNode(makeTargetSelector(stateNodes, source)) as Record, - branches: (declarations: unknown) => decorateBranches(declarations) - }) - -const normalizeTransitionBuilder = ( - transition: (selector: unknown) => unknown, - stateNodes: Machine.StateNodes, - path: string -): unknown => { - const result = transition(makeTransitionSelector(stateNodes, path)) - if (Topology.isTargetSelection(result) && !hasProperty(result, TransitionBuilderDescriptorTypeId)) { - const selection = plainTargetSelection(result) - return { target: () => selection } - } - if (!hasProperty(result, TransitionBuilderDescriptorTypeId)) return result - const descriptor = result as TransitionBuilderDescriptor - if (descriptor.type === "branches") { - return { - branches: () => descriptor.declarations, - resolve: descriptor.resolve, - reenter: descriptor.reenterSource, - declinable: descriptor.declinable - } - } - return { - target: () => descriptor.selection, - resolve: descriptor.resolver, - reenter: descriptor.reenterSource, - declinable: descriptor.declinable - } -} - const transitionTargetSelection = ( selection: Topology.TargetSelection ): Machine.TransitionTargetSelection => @@ -416,13 +221,6 @@ const selectionUpdates = (selection: Topology.TargetSelection): ReadonlyArray Topology.TargetSelection => -() => Topology.makeTargetSelection(kind, path, scope) - const makeSelectionValue = ( kind: Topology.TargetSelectionKind, path: string | undefined, @@ -434,106 +232,6 @@ const makeStateUpdateSelection = ( scope: "local" | "branch" ): Topology.TargetSelection => Topology.makeTargetSelection("update", path, scope) -const addSelectionChildren = ( - builder: Record, - stateNodes: Machine.StateNodes, - parent: string, - scope: "local" | "branch", - source?: string -): void => { - for (const node of stateNodes.byPath.values()) { - if (node.parent !== parent || node.type === "history") continue - builder[node.key] = makeSelectionNode(stateNodes, node.path, scope, source) - } -} - -const makeSelectionNode = ( - stateNodes: Machine.StateNodes, - path: string, - scope: Topology.TargetSelectionScope, - source?: string -): unknown => { - const node = getTargetBuilderNode(stateNodes, path) - const kind: Topology.TargetSelectionKind = node.type === "choice" ? "choice" : "state" - const method = makeSelectionMethod(kind, path, scope) as unknown as Record - if (node.type !== "atomic" && node.type !== "final" && node.type !== "choice" && node.type !== "history") { - Object.defineProperty(method, "initial", { - value: makeSelectionValue("initial", path, scope), - enumerable: true - }) - if (scope === "local" || scope === "branch") { - addSelectionChildren(method, stateNodes, path, scope, source) - } - if ( - scope === "branch" && source !== undefined && node.schema !== undefined && - (source === path || Configuration.isDescendantOf(source, path)) && - getTargetBuilderNode(stateNodes, source).type !== "choice" - ) { - Object.defineProperty(method, "update", { - value: makeStateUpdateSelection(path, "branch"), - enumerable: true - }) - } - } - if (node.type === "atomic" && scope === "branch" && source === path && node.schema !== undefined) { - Object.defineProperty(method, "update", { value: makeStateUpdateSelection(path, "branch"), enumerable: true }) - } - return method -} - -const makeHistorySelectionTree = ( - stateNodes: Machine.StateNodes, - parent: string | undefined -): Record => { - const builder: Record = {} - for (const node of stateNodes.byPath.values()) { - if (node.parent !== parent) continue - if (node.type === "history") { - builder[node.key] = makeSelectionValue("history", node.path, "full") - } else if (node.type !== "choice") { - const children = makeHistorySelectionTree(stateNodes, node.path) - if (Object.keys(children).length > 0) builder[node.key] = children - } - } - return builder -} - -const makeTargetSelector = ( - stateNodes: Machine.StateNodes, - source: string -): unknown => { - const full = Object.assign({}, makeSelectionValue("state", "", "full"), { - initial: makeSelectionValue("initial", "", "full") - }) - const branch: Record = {} - const root = getTargetBuilderNode(stateNodes, stateNodes.roots[0]!) - addSelectionChildren(branch, stateNodes, root.path, "branch", source) - const local: Record = {} - const localScope = getLocalTargetScope(stateNodes, source) - if (localScope !== undefined) { - const localScopeNode = getTargetBuilderNode(stateNodes, localScope) - if (localScopeNode.schema !== undefined) { - local.with = makeSelectionValue("state", localScope, "local") - if (getTargetBuilderNode(stateNodes, source).type !== "choice") { - local.update = makeStateUpdateSelection(localScope, "local") - } - } - addSelectionChildren(local, stateNodes, localScope, "local") - } - return { - none: Topology.noneTargetSelection, - self: getTargetBuilderNode(stateNodes, source).schema === undefined || - getTargetBuilderNode(stateNodes, source).type === "final" - ? {} - : { update: makeStateUpdateSelection(source, "branch") }, - root: makeSelectionNode(stateNodes, root.path, "branch", source), - local, - branch, - full, - history: makeHistorySelectionTree(stateNodes, "") - } -} - const captureDefinitionBranch = ( branch: unknown, selector: unknown, @@ -777,14 +475,6 @@ const captureNamedBranches = ( if (!Topology.isTargetSelection(target)) { throw new Error(`Machine transition branch "${key}" must select exactly one target`) } - if (target.updatePath !== undefined) { - throw new Error(`Machine transition branch "${key}" cannot declare an updating target`) - } - if (hasProperty(target, TransitionBuilderDescriptorTypeId)) { - throw new Error( - `Machine transition branch "${key}" requires a topology selection; apply .reenter() to .branches(...)` - ) - } if (title !== undefined && (typeof title !== "string" || title.length === 0)) { throw new Error(`Machine transition branch "${key}" title must be a non-empty string`) } @@ -796,11 +486,32 @@ const wrapSelectedBranchBuilder = ( builder: unknown, owner: object, branchIndex: number, - branchKey: string + branchKey: string, + updateBuilder?: Record unknown> ): unknown => { if (typeof builder === "function") { - const wrapped = (...args: ReadonlyArray) => - Topology.makeSelectedBranch(owner, branchIndex, branchKey, builder(...args)) + const wrapped = (...args: ReadonlyArray) => { + const result = builder(...args) + if (updateBuilder !== undefined) { + return Object.freeze({ + update: Object.freeze( + Object.fromEntries( + Object.getOwnPropertyNames(updateBuilder).map(( + method + ) => [method, (value: unknown) => + Topology.makeSelectedBranch( + owner, + branchIndex, + branchKey, + result.update(updateBuilder[method]!(value)) + )] + ) + ) + ) + }) + } + return Topology.makeSelectedBranch(owner, branchIndex, branchKey, result) + } for (const property of Reflect.ownKeys(builder)) { if ( property === "length" || property === "name" || property === "prototype" || property === "caller" || @@ -809,7 +520,7 @@ const wrapSelectedBranchBuilder = ( const descriptor = Object.getOwnPropertyDescriptor(builder, property) if (descriptor === undefined) continue if ("value" in descriptor && typeof descriptor.value === "function") { - descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey) + descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey, updateBuilder) } Object.defineProperty(wrapped, property, descriptor) } @@ -821,7 +532,7 @@ const wrapSelectedBranchBuilder = ( const descriptor = Object.getOwnPropertyDescriptor(builder, property) if (descriptor === undefined) continue if ("value" in descriptor && typeof descriptor.value === "function") { - descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey) + descriptor.value = wrapSelectedBranchBuilder(descriptor.value, owner, branchIndex, branchKey, updateBuilder) } Object.defineProperty(wrapped, property, descriptor) } @@ -844,7 +555,15 @@ const makeBranchSelectors = ( getSelectionBuilder(context.target, branch.selection, stateNodes, source), owner, branchIndex, - branch.key + branch.key, + branch.selection.updatePath === undefined + ? undefined + : getSelectionBuilder( + context.target, + makeStateUpdateSelection(branch.selection.updatePath, "branch"), + stateNodes, + source + ) as Record unknown> ) } return Object.freeze(select) @@ -867,23 +586,119 @@ const validateSelectedBranchResult = ( validateResolvedSelection(result, selection, stateNodes) } +// All source-dependent schema relationships have been checked by the public types. +const objectContext = (context: Record, path: string): Record => ({ + ...context, + root: path === "" ? context.state : context.ancestors[""] +}) +const normalizeObjectTransition = ( + raw: unknown, + declaration: Declaration.Declaration, + path: string, + stateNodes: Machine.StateNodes +): Record => { + if (typeof raw !== "object" || raw === null) throw new Error(`Machine transition for "${path}" must be an object`) + const config = { ...raw } as Record + const allowed = [ + "target", + "update", + "initial", + "history", + "none", + "branches", + "from", + "decoded", + "resolve", + "guard", + "reenter", + "declinable" + ] + for (const key of Reflect.ownKeys(config)) { + if (typeof key !== "string" || !allowed.includes(key)) { + throw new Error("Machine transition contains an unknown field") + } + } + for (const key of ["from", "decoded", "resolve", "guard"]) { + if (config[key] !== undefined && typeof config[key] !== "function") { + throw new Error(`Machine transition ${key} must be a function`) + } + } + const guard = typeof config.guard === "function" ? config.guard : undefined + const resolve = typeof config.resolve === "function" ? config.resolve : undefined + const construct = typeof config.from === "function" + ? config.from + : typeof config.decoded === "function" + ? config.decoded + : undefined + const methods = ["from", "decoded", "resolve"].filter((key) => config[key] !== undefined) + if (methods.length > 1) throw new Error("Machine transition construction methods are mutually exclusive") + const guarded = guard !== undefined + const declinable = guarded || config.declinable === true + if (typeof config.branches === "string") { + const group = declaration.branches.get(config.branches) + if (group === undefined || resolve === undefined) { + throw new Error("Machine branching transition requires a registered group and resolver") + } + if ( + ["target", "update", "initial", "history", "none", "from", "decoded"].some((key) => config[key] !== undefined) + ) throw new Error("Machine branching transition cannot redeclare its destination") + const entries = Object.fromEntries( + Object.entries(group).map(([key, spec]) => [key, { + title: spec.title, + target: captureDefinitionBranch( + { target: () => Declaration.selection(declaration, spec) }, + undefined, + stateNodes, + path, + key + ).selection + }]) + ) + return { + branches: () => entries, + reenter: config.reenter, + declinable, + resolve: (context: Record, enqueue: unknown) => { + const ctx = { ...objectContext(context, path), decline: Topology.makeDeclined } + return guarded && !guard(ctx) ? Topology.makeDeclined() : resolve(ctx, enqueue) + } + } + } + const selection = Declaration.selection(declaration, config) + if (resolve !== undefined && selection.kind !== "none") { + throw new Error("Machine advanced construction requires a declared branch group") + } + const method = config.from !== undefined ? "from" : config.decoded !== undefined ? "decoded" : undefined + return { + target: () => selection, + reenter: config.reenter, + declinable, + ...(!guarded && method === undefined && resolve === undefined ? {} : { + resolve: (context: Record, enqueue: unknown) => { + const ctx = objectContext(context, path) + if (guarded && !guard(ctx)) return Topology.makeDeclined() + if (method !== undefined && construct !== undefined) { + return constructSelectionValue(selection, context, method, construct(ctx)) + } + if (resolve !== undefined) return resolve({ ...ctx, decline: Topology.makeDeclined }, enqueue) + return selection.kind === "none" ? undefined : constructSelectedTarget(context.target) + } + }) + } +} + const captureTransition = ( rawTransition: unknown, stateNodes: Machine.StateNodes, path: string, + declaration: Declaration.Declaration, trigger: PropertyKey ): unknown => { - if (typeof rawTransition !== "function") { - throw new Error( - `Machine transition for state "${path}" on "${String(trigger)}" must be a target-first callback` - ) - } - const transition = normalizeTransitionBuilder(rawTransition as (selector: unknown) => unknown, stateNodes, path) + const transition = normalizeObjectTransition(rawTransition, declaration, path, stateNodes) if (typeof transition !== "object" || transition === null) { throw new Error(`Machine transition for state "${path}" on "${String(trigger)}" must be an object`) } const definition = transition as Record - const selector = makeTargetSelector(stateNodes, path) const reenter = definition.reenter === true const declinable = definition.declinable === true if (hasProperty(definition, "branches")) { @@ -894,7 +709,7 @@ const captureTransition = ( ) } const resolve = branching.resolve - const branches = captureNamedBranches(branching.branches(selector), path, trigger) + const branches = captureNamedBranches(branching.branches(), path, trigger) const owner = Object.freeze({}) const evaluate = (context: Record, enqueue: unknown) => { const resolverContext = { ...context } @@ -952,7 +767,7 @@ const captureTransition = ( transition: (context: Record, enqueue: unknown) => evaluate(context, enqueue).result } } - const branch = captureDefinitionBranch(transition, selector, stateNodes, path, trigger) + const branch = captureDefinitionBranch(transition, undefined, stateNodes, path, trigger) const evaluate = (context: Record, enqueue: unknown) => ({ result: runCapturedBranch(branch, context, enqueue, stateNodes, path), branchIndex: 0, @@ -976,86 +791,47 @@ const captureTransition = ( const captureEventHandlers = ( on: object, stateNodes: Machine.StateNodes, - path: string + path: string, + declaration: Declaration.Declaration ): Record => { // The machine owns its dispatch table. Compiled plans may snapshot these // definitions, so retaining caller-owned containers would let strategies // observe different handlers after an unsafe external mutation. const captured: Record = Object.create(null) for (const event of Reflect.ownKeys(on)) { - captured[event] = captureTransition((on as Record)[event], stateNodes, path, event) + captured[event] = captureTransition( + (on as Record)[event], + stateNodes, + path, + declaration, + event + ) } return captured } -interface InvokeBuilderDescriptor { - readonly [InvokeBuilderDescriptorTypeId]: typeof InvokeBuilderDescriptorTypeId - readonly config: Readonly> -} - -type InvokeBuilderChannel = "onDone" | "onFailure" | "onElement" | "onSnapshot" - -const makeInvokeBuilder = ( - config: Readonly>, - channels: ReadonlyArray -): InvokeBuilderDescriptor => { - const builder: Record = { - [InvokeBuilderDescriptorTypeId]: InvokeBuilderDescriptorTypeId as typeof InvokeBuilderDescriptorTypeId, - config - } - for (const channel of channels) { - if (!hasProperty(config, channel)) { - builder[channel] = (handler: unknown) => makeInvokeBuilder({ ...config, [channel]: handler }, channels) - } - } - return Object.freeze(builder) as unknown as InvokeBuilderDescriptor -} - -const invokeSelector = Object.freeze({ - effect: (id: string, effect: unknown) => makeInvokeBuilder({ id, effect }, ["onDone", "onFailure"]), - stream: (id: string, stream: unknown) => makeInvokeBuilder({ id, stream }, ["onElement", "onDone", "onFailure"]), - timer: (id: string, after: unknown) => makeInvokeBuilder({ id, after }, ["onDone"]), - logic: (id: string, options: Readonly>) => - makeInvokeBuilder({ id, ...options }, ["onSnapshot", "onDone", "onFailure"]), - child: (child: unknown, options?: Readonly>) => - makeInvokeBuilder(options === undefined ? { child } : { child, ...options }, [ - "onSnapshot", - "onDone", - "onFailure" - ]) -}) - -const invokeBuilderConfig = (value: unknown, path: string): Readonly> => { - if ( - typeof value !== "object" || value === null || - !hasProperty(value, InvokeBuilderDescriptorTypeId) || - value[InvokeBuilderDescriptorTypeId] !== InvokeBuilderDescriptorTypeId - ) { - throw new Error(`Machine invocation for state "${path}" must be constructed from its source selector`) - } - return (value as unknown as InvokeBuilderDescriptor).config -} - const captureInvokeDefinition = ( invoke: unknown, stateNodes: Machine.StateNodes, - path: string + path: string, + declaration: Declaration.Declaration ): unknown => { - if (typeof invoke !== "function") { - throw new Error(`Machine invocation for state "${path}" must be a source-first callback`) - } - const authored = invoke(invokeSelector) - const definitions = Array.isArray(authored) ? authored : [authored] + const definitions = Array.isArray(invoke) ? invoke : [invoke] const capturedDefinitions = definitions.map((definition) => { - const captured = { ...invokeBuilderConfig(definition, path) } + const captured = Declaration.invocation( + declaration, + definition, + path, + (context) => objectContext(context as Record, path) + ) for (const key of ["onElement", "onDone", "onFailure", "onSnapshot"] as const) { if (captured[key] !== undefined) { - captured[key] = captureTransition(captured[key], stateNodes, path, key) + captured[key] = captureTransition(captured[key], stateNodes, path, declaration, key) } } return InvocationDefinition.capture(captured, path) }) - if (Array.isArray(authored)) return Object.freeze(capturedDefinitions) + if (Array.isArray(invoke)) return Object.freeze(capturedDefinitions) return capturedDefinitions[0] } @@ -1064,6 +840,7 @@ const flattenHandlers = ( stateNodes: Machine.StateNodes, states: Machine.StateTree, prefix: string, + declaration: Declaration.Declaration, config: Record ): void => { for (const key of Object.keys(config)) { @@ -1086,20 +863,20 @@ const flattenHandlers = ( } const on = stateConfig.on if (typeof on === "object" && on !== null) { - const capturedOn = captureEventHandlers(on, stateNodes, path) + const capturedOn = captureEventHandlers(on, stateNodes, path, declaration) stateConfig.on = capturedOn } if (stateConfig.always !== undefined) { - stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, "always") + stateConfig.always = captureTransition(stateConfig.always, stateNodes, path, declaration, "always") } if (stateConfig.onDone !== undefined) { - stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, "done") + stateConfig.onDone = captureTransition(stateConfig.onDone, stateNodes, path, declaration, "done") } if (stateConfig.choice !== undefined) { - stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, "choice") + stateConfig.choice = captureTransition(stateConfig.choice, stateNodes, path, declaration, "choice") } if (stateConfig.invoke !== undefined) { - stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path) + stateConfig.invoke = captureInvokeDefinition(stateConfig.invoke, stateNodes, path, declaration) } const node = stateNodes.byPath.get(path) if (node?.type === "choice") { @@ -1121,15 +898,15 @@ const flattenHandlers = ( if (typeof childConfig !== "object" || childConfig === null) { throw new Error(`Machine expected state "${path}" child handlers to be an object`) } - flattenHandlers(handlers, stateNodes, node.states, path, childConfig as Record) + flattenHandlers(handlers, stateNodes, node.states, path, declaration, childConfig as Record) } } } -const makeHandle = (self: Definition.Any): Definition.Any["handle"] => +const makeHandle = (self: Definition.Any, declaration: Declaration.Declaration): Definition.Any["handle"] => ((config: Record) => { const handlers: Record = Object.create(null) - flattenHandlers(handlers, self.stateNodes, self.states, "", { "": config }) + flattenHandlers(handlers, self.stateNodes, self.states, "", declaration, { "": config }) return makeWithHandlers(self, handlers) }) as Definition.Any["handle"] @@ -1326,6 +1103,12 @@ export const make = < config: { readonly id?: string readonly root: State + readonly effects?: unknown + readonly streams?: unknown + readonly timers?: unknown + readonly logic?: unknown + readonly children?: unknown + readonly branches?: unknown readonly initialConfiguration?: unknown readonly events: Machine.EventProtocol<"public", InputEvents> readonly internalEvents?: Machine.EventProtocol<"internal", InternalEvents> @@ -1356,7 +1139,7 @@ export const make = < self.initialDefinition = compiledInitial.definition self.makeTargetBuilder = makeTargetBuilder(states, self.stateNodes) self.handlers = Object.create(null) - self.handle = makeHandle(self) + self.handle = makeHandle(self, Declaration.capture(config.root, config)) Protocol.setProtocol(self) return self } diff --git a/packages/effect-machine/src/internal/machine/planner.ts b/packages/effect-machine/src/internal/machine/planner.ts index c1031be5..ae211f8c 100644 --- a/packages/effect-machine/src/internal/machine/planner.ts +++ b/packages/effect-machine/src/internal/machine/planner.ts @@ -710,7 +710,7 @@ const makeTransitionContext = < path: string, event: Machine.EventByTag, snapshot: Machine.Snapshot -): Machine.HandlerContext => ({ +): ConstructionContext> => ({ ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier, containingState: getParentValue(machine, configuration, path) as Machine.ParentStateValue, @@ -732,7 +732,7 @@ const makeDoneContext = < event: Machine.LifecycleEvent, output: unknown, snapshot: Machine.Snapshot -): Machine.DoneContext => ({ +): ConstructionContext> => ({ ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier, containingState: getParentValue(machine, configuration, path) as Machine.ParentStateValue, @@ -797,7 +797,7 @@ const selectAlwaysTransitions = < States, E, R, - Machine.AlwaysContext> + ConstructionContext>> > > => { const selected: Array< @@ -805,7 +805,7 @@ const selectAlwaysTransitions = < States, E, R, - Machine.AlwaysContext> + ConstructionContext>> > > = [] const selectedSources = new Set() @@ -826,7 +826,7 @@ const selectAlwaysTransitions = < States, E, R, - Machine.AlwaysContext> + ConstructionContext>> >, context: { ...resolveMachineReferences(machine, configuration), @@ -877,7 +877,7 @@ const selectDoneTransitions = < States, E, R, - Machine.DoneContext> + ConstructionContext>> > > => { const selected: Array< @@ -885,7 +885,7 @@ const selectDoneTransitions = < States, E, R, - Machine.DoneContext> + ConstructionContext>> > > = [] const selectedSources = new Set() @@ -903,7 +903,7 @@ const selectDoneTransitions = < States, E, R, - Machine.DoneContext> + ConstructionContext>> >, context: makeDoneContext>( machine, @@ -935,14 +935,7 @@ const selectEventTransitions = < States, E, R, - Machine.HandlerContext, Machine.TagOf, E, R> - > -> => { - const selected: Array< - SelectedTransition< - States, - E, - R, + ConstructionContext< Machine.HandlerContext< States, Events, @@ -953,6 +946,25 @@ const selectEventTransitions = < R > > + > +> => { + const selected: Array< + SelectedTransition< + States, + E, + R, + ConstructionContext< + Machine.HandlerContext< + States, + Events, + Emits, + Machine.StateIdentifier, + Machine.TagOf, + E, + R + > + > + > > = [] const selectedSources = new Set() const evaluatedSources = new Map | undefined>() @@ -972,14 +984,16 @@ const selectEventTransitions = < States, E, R, - Machine.HandlerContext< - States, - Events, - Emits, - Machine.StateIdentifier, - Machine.TagOf, - E, - R + ConstructionContext< + Machine.HandlerContext< + States, + Events, + Emits, + Machine.StateIdentifier, + Machine.TagOf, + E, + R + > > >, context: makeTransitionContext< @@ -2357,3 +2371,7 @@ export const planInitial = ( : new StartupError({ cause: Cause.die(error) }) } }) + +// Captured constructors are private planner inputs. Public callbacks receive root data +// and the constructors bound to their declared branches during normalization. +type ConstructionContext = Omit & { readonly target: unknown } diff --git a/packages/effect-machine/src/internal/machine/targetReference.ts b/packages/effect-machine/src/internal/machine/targetReference.ts new file mode 100644 index 00000000..a7047457 --- /dev/null +++ b/packages/effect-machine/src/internal/machine/targetReference.ts @@ -0,0 +1,40 @@ +import type { Machine, State } from "../../Machine.js" +import * as Topology from "./topology.js" + +/** Reference metadata is separate from child names, including names such as `path`. */ +export const TypeId: unique symbol = Symbol.for("effect/Machine/TargetReference") + +export interface Reference { + readonly [TypeId]: { + readonly root: State + readonly path: string + readonly kind: "state" | "choice" | "history" + } +} + +/** Captures an immutable reference tree without allocating or executing a machine. */ +export const make = (root: State): { readonly root: Reference } => { + const nodes = Topology.compileStateNodes({ "": root.node }) + const references = new Map() + for (const node of nodes.byPath.values()) { + const reference = Object.create(null) + Object.defineProperty(reference, TypeId, { + value: Object.freeze({ + root, + path: node.path, + kind: node.type === "history" || node.type === "choice" ? node.type : "state" + }) + }) + references.set(node.path, reference) + } + for (const node of nodes.byPath.values()) { + if (node.parent !== undefined) { + Object.defineProperty(references.get(node.parent), node.key, { + value: references.get(node.path), + enumerable: true + }) + } + } + for (const reference of references.values()) Object.freeze(reference) + return Object.freeze({ root: references.get("")! }) +} diff --git a/packages/effect-machine/src/internal/testing/machine/finiteModel.ts b/packages/effect-machine/src/internal/testing/machine/finiteModel.ts index 3769afd6..f06b3910 100644 --- a/packages/effect-machine/src/internal/testing/machine/finiteModel.ts +++ b/packages/effect-machine/src/internal/testing/machine/finiteModel.ts @@ -1315,13 +1315,6 @@ const findSnapshot = (snapshot: unknown, path: string): unknown => { return undefined } -const selectHistoryTarget = (builder: Record, path: string): unknown => { - const parts = path.split(".") - let current: any = builder - for (let index = 0; index < parts.length - 1; index++) current = current[parts[index]!] - return current[parts[parts.length - 1]!] -} - const selectableDefinitionTarget = ( source: string, target: string, @@ -1340,26 +1333,20 @@ const selectableDefinitionTarget = ( } const selectDefinitionTarget = ( - selector: Record, + references: Record, source: string, target: string, byPath: ReadonlyMap -): unknown => { +): Readonly> => { const selected = byPath.get(target)! - if (selected.node._tag === "History") { - return selectHistoryTarget(selector.history, target) - } - const sourceRoot = byPath.get(source)!.root - if (selected.root !== sourceRoot) { - const root = target.split(".")[0]! - return selector.branch[root]() - } - const selectable = selectableDefinitionTarget(source, target, byPath) - let current = selector.branch - const parts = selectable.split(".") - for (const part of parts) current = current[part] - if (typeof current === "function") return current() - return current.initial + const selectedPath = selected.node._tag === "History" ? + target + : selected.root !== byPath.get(source)!.root ? + target.split(".")[0]! + : selectableDefinitionTarget(source, target, byPath) + let reference = references + for (const part of selectedPath.split(".")) reference = reference[part] + return selected.node._tag === "History" ? { history: reference } : { target: reference } } const resolveDefinitionTarget = ( @@ -1410,18 +1397,23 @@ const makeHandlers = ( states: ReadonlyArray, parent: string | undefined, byPath: ReadonlyMap, - transitions: ReadonlyMap + transitions: ReadonlyMap, + references: Record, + branches: Record>> ): Record => { const handlers: Record = Object.create(null) for (const node of states) { const path = parent === undefined ? node.key : `${parent}.${node.key}` if (node._tag === "History") continue if (node._tag === "Choice") { + const group = `choice:${path}` + branches[group] = { destination: selectDefinitionTarget(references, path, node.selected, byPath) } handlers[node.key] = { - choice: (to: Record) => - (selectDefinitionTarget(to, path, node.selected, byPath) as any).resolve( - ({ target }: { readonly target: any }) => resolveDefinitionTarget(target, path, node.selected, byPath) - ) + choice: { + branches: group, + resolve: ({ select }: { readonly select: any }) => + resolveDefinitionTarget(select.destination, path, node.selected, byPath) + } } continue } @@ -1434,15 +1426,19 @@ const makeHandlers = ( let onDone: unknown for (const transition of transitions.values()) { if (transition.source !== path) continue - const config = (to: Record) => { - const selected = transition.target === undefined - ? to.none - : selectDefinitionTarget(to, path, transition.target, byPath) - const resolve = transition.target === undefined - ? () => undefined - : ({ target }: { readonly target: any }) => - resolveDefinitionTarget(target, path, transition.target!, byPath, transition.targetValue) - return ("reenter" in transition && transition.reenter ? selected.reenter() : selected).resolve(resolve) + const group = `transition:${path}:${triggerKey(transition.trigger)}` + if (transition.target !== undefined) { + branches[group] = { + destination: selectDefinitionTarget(references, path, transition.target, byPath) + } + } + const config = { + ...(transition.target === undefined ? { none: true } : { + branches: group, + resolve: ({ select }: { readonly select: any }) => + resolveDefinitionTarget(select.destination, path, transition.target!, byPath, transition.targetValue) + }), + ...("reenter" in transition && transition.reenter ? { reenter: true } : {}) } if (transition.trigger.type === "event") on[transition.trigger.event] = config else if (transition.trigger.type === "always") always = config @@ -1489,7 +1485,7 @@ const makeHandlers = ( builder ) }), - states: makeHandlers(node.states, path, byPath, transitions) + states: makeHandlers(node.states, path, byPath, transitions, references, branches) } : {}) } @@ -1515,18 +1511,21 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => { const defined = Machine.state({ initial: model.initial, states: stateTree } as any) const eventSchemas = model.events.map((event) => Schema.TaggedStruct(event, {})) const initial = byPath.get(model.initial)! - const machine = Machine.make({ - root: defined, - events: Machine.eventsFromSchemas(...eventSchemas) as any - } as any) const transitions = new Map(model.transitions.map((transition) => [ `${transition.source}\u0000${triggerKey(transition.trigger)}`, transition ])) + const branches: Record>> = Object.create(null) + const handlers = makeHandlers(model.roots, undefined, byPath, transitions, Machine.targets(defined).root, branches) + const machine = Machine.make({ + root: defined, + branches, + events: Machine.eventsFromSchemas(...eventSchemas) as any + } as any) return machine.handle( { initialize: ({ builder }: any) => builder.decoded(stateValue(initial)), - states: makeHandlers(model.roots, undefined, byPath, transitions) + states: handlers } as any ) as Machine.Machine.Any } diff --git a/packages/effect-machine/test/examples/can.ts b/packages/effect-machine/test/examples/can.ts index 98fec5e9..db9c34b6 100644 --- a/packages/effect-machine/test/examples/can.ts +++ b/packages/effect-machine/test/examples/can.ts @@ -7,7 +7,7 @@ const machine = Machine.make({ events: Machine.eventsFromSchemas(), internalEvents, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.from())) -}).handle({ states: { Idle: { on: { Loaded: (to) => to.none } } } }) +}).handle({ states: { Idle: { on: { Loaded: { none: true } } } } }) export const canLoad = Effect.gen(function*() { const initial = yield* Machine.planInitial(machine) diff --git a/packages/effect-machine/test/examples/guard.ts b/packages/effect-machine/test/examples/guard.ts index 9b48362e..1e7e823f 100644 --- a/packages/effect-machine/test/examples/guard.ts +++ b/packages/effect-machine/test/examples/guard.ts @@ -2,15 +2,18 @@ import { Machine } from "@typeonce/effect-machine" import { Schema } from "effect" const events = Machine.events({ Add: { by: Schema.Number } }) +const Root = Machine.state({ fields: { count: Schema.Number } }) +const targets = Machine.targets(Root) export const counter = Machine.make({ - root: Machine.state({ fields: { count: Schema.Number } }), + root: Root, events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ on: { - Add: (to) => - to.self.update.guard(({ event }) => event.by > 0).from(({ current, event }) => ({ - count: current.count + event.by - })) + Add: { + update: targets.root, + guard: ({ event }) => event.by > 0, + from: ({ root, event }) => ({ count: root.count + event.by }) + } } }) diff --git a/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts b/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts index f61a232c..956e1271 100644 --- a/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts +++ b/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts @@ -11,8 +11,9 @@ describe("event validation across execution strategies", () => { const Set = Schema.TaggedStruct("Set", { value: Schema.Int }) const events = Machine.eventsFromSchemas(Set) let retained: unknown + const root1 = Machine.state({ initial: "Ready", states: { Ready: Schema.TaggedStruct("Ready", {}) } }) const machine = Machine.make({ - root: Machine.state({ initial: "Ready", states: { Ready: Schema.TaggedStruct("Ready", {}) } }), + root: root1, events, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Ready.decoded({ _tag: "Ready" }))) @@ -20,10 +21,12 @@ describe("event validation across execution strategies", () => { states: { Ready: { on: { - Set: (to) => - to.none.resolve(({ event }) => { + Set: { + none: true, + resolve: ({ event }) => { retained = event - }) + } + } } } } diff --git a/packages/effect-machine/test/internal/machine/activities.test.ts b/packages/effect-machine/test/internal/machine/activities.test.ts index f940df54..92a7a17c 100644 --- a/packages/effect-machine/test/internal/machine/activities.test.ts +++ b/packages/effect-machine/test/internal/machine/activities.test.ts @@ -27,6 +27,21 @@ let dynamicFactoryEvaluations = 0 const timerDuration = "10 seconds" const activityStates = Machine.state({ initial: "Loading", states: { Loading, Dynamic } }) const activityMachine = Machine.make({ + effects: { source2: Effect.suspend(() => Effect.fail("unavailable").pipe(Effect.as(1))) }, + streams: { source4: Stream.suspend(() => Stream.empty) }, + timers: { source3: timerDuration }, + logic: { + dynamic: (_input: undefined) => { + dynamicFactoryEvaluations++ + return Machine.logic({ initial: undefined, run: () => Effect.never }) + }, + source1: Machine.logic({ + initial: undefined, + run: () => Effect.never + }) + }, + children: { source5: child }, + id: "activity-inspection", root: activityStates, events: Machine.eventsFromSchemas(WorkSucceeded, WorkFailed, LoadTimedOut), @@ -34,32 +49,21 @@ const activityMachine = Machine.make({ }).handle({ states: { Loading: { - invoke: ( - from - ) => [ - from.logic("poll-server", { - address: Machine.childAddress("poll-server"), - logic: Machine.logic({ - initial: undefined, - run: () => Effect.never - }) - }), - from.effect("load-document", () => Effect.fail("unavailable").pipe(Effect.as(1))).onDone((to) => to.none) - .onFailure((to) => to.none), - from.timer("load-timeout", timerDuration).onDone((to) => to.none), - from.stream("updates", () => Stream.empty).onDone((to) => to.none), - from.child(child) + invoke: [ + { src: "source1", id: "poll-server", address: Machine.childAddress("poll-server") }, + { src: "source2", id: "load-document", onDone: { none: true }, onFailure: { none: true } }, + { src: "source3", id: "load-timeout", onDone: { none: true } }, + { src: "source4", id: "updates", onDone: { none: true } }, + { src: "source5" } ] }, Dynamic: { - invoke: (from) => - from.logic("context-owned", { - address: Machine.childAddress("context-owned"), - logic: () => { - dynamicFactoryEvaluations++ - return Machine.logic({ initial: undefined, run: () => Effect.never }) - } - }) + invoke: { + src: "dynamic", + id: "context-owned", + address: Machine.childAddress("context-owned"), + input: () => undefined + } } } }) @@ -169,6 +173,8 @@ describe("machine activity metadata", () => { Effect.sync(() => { const id = `generated-timer-${idSuffix}` const generated = Machine.make({ + timers: { source1: durationMillis }, + root: activityStates, events: Machine.eventsFromSchemas(LoadTimedOut), initialConfiguration: (root) => @@ -176,7 +182,7 @@ describe("machine activity metadata", () => { }).handle({ states: { Loading: { - invoke: (from) => from.timer(id, durationMillis).onDone((to) => to.none) + invoke: { src: "source1", id: id, onDone: { none: true } } } } }) diff --git a/packages/effect-machine/test/internal/machine/rootStrategies.test.ts b/packages/effect-machine/test/internal/machine/rootStrategies.test.ts index 9a832246..b1a694ac 100644 --- a/packages/effect-machine/test/internal/machine/rootStrategies.test.ts +++ b/packages/effect-machine/test/internal/machine/rootStrategies.test.ts @@ -23,20 +23,24 @@ it.effect("constructs schema defaults throughout a parallel root", () => { }) it.effect("compares structural leaves, reentry, raised events and completion with generic planning", () => { + const root1 = Machine.state({ initial: "Idle", states: { Idle: {}, Busy: {}, Done: { type: "final" } } }) + const targets1 = Machine.targets(root1) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle: {}, Busy: {}, Done: { type: "final" } } }), + root: root1, events: Machine.events({ Begin: {}, Reenter: {}, Finish: {}, Complete: {} }) }).handle({ states: { - Idle: { on: { Begin: (to) => to.local.Busy() } }, + Idle: { on: { Begin: { target: targets1.root.Busy } } }, Busy: { on: { - Reenter: (to) => to.none.reenter(), - Finish: (to) => - to.none.resolve((_, enqueue) => { + Reenter: { none: true, reenter: true }, + Finish: { + none: true, + resolve: (_, enqueue) => { enqueue.raise({ _tag: "Complete" }) - }), - Complete: (to) => to.local.Done() + } + }, + Complete: { target: targets1.root.Done } } } } @@ -55,15 +59,16 @@ it.effect("compares structural leaves, reentry, raised events and completion wit it.effect("keeps flat root updates and retained snapshots equal to generic planning", () => { const root = Machine.state({ fields: { count: Schema.Number } }) + const targets2 = Machine.targets(root) const machine = Machine.make({ root, events: Machine.events({ Increment: { by: Schema.Number }, Noop: {}, Reenter: {} }), initial: (root) => root.from(() => ({ count: 0 })) }).handle({ on: { - Increment: (to) => to.self.update.from(({ current, event }) => ({ count: current.count + event.by })), - Noop: (to) => to.none, - Reenter: (to) => to.none.reenter() + Increment: { update: targets2.root, from: ({ root: current, event }) => ({ count: current.count + event.by }) }, + Noop: { none: true }, + Reenter: { none: true, reenter: true } } }) return verifyPlannerStrategies({ @@ -81,15 +86,18 @@ it.effect("keeps flat root updates and retained snapshots equal to generic plann it.effect("keeps root values and child transitions equal to generic planning", () => { const root = Machine.state({ fields: { count: Schema.Number }, initial: "Idle", states: { Idle: {}, Busy: {} } }) + const targets3 = Machine.targets(root) const machine = Machine.make({ root, events: Machine.events({ Increment: { by: Schema.Number }, Start: {}, Stop: {} }), initial: (root) => root.from(() => ({ count: 0 })) }).handle({ - on: { Increment: (to) => to.self.update.from(({ current, event }) => ({ count: current.count + event.by })) }, + on: { + Increment: { update: targets3.root, from: ({ root: current, event }) => ({ count: current.count + event.by }) } + }, states: { - Idle: { on: { Start: (to) => to.local.Busy() } }, - Busy: { on: { Stop: (to) => to.local.Idle() } } + Idle: { on: { Start: { target: targets3.root.Busy } } }, + Busy: { on: { Stop: { target: targets3.root.Idle } } } } }) return verifyPlannerStrategies({ diff --git a/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts b/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts index 2604c746..12ff7072 100644 --- a/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts +++ b/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts @@ -35,6 +35,7 @@ const makeFlatMachine = () => { Done: { schema: Done, type: "final", output: Schema.Number } } }) + const targets1 = Machine.targets(states) return Machine.make({ root: states, events: Machine.eventsFromSchemas(Noop, Increment, Reenter, Finish), @@ -44,12 +45,10 @@ const makeFlatMachine = () => { states: { Count: { on: { - Noop: (to) => to.none, - Increment: (to) => - to.branch.Count().resolve(({ state, target }) => target.decoded(new Count({ value: state.value + 1 }))), - Reenter: (to) => to.none.reenter().resolve(() => undefined), - Finish: (to) => - to.branch.Done().resolve(({ state, target }) => target.decoded(new Done({ value: state.value }))) + Noop: { none: true }, + Increment: { target: targets1.root.Count, decoded: ({ state }) => (new Count({ value: state.value + 1 })) }, + Reenter: { none: true, reenter: true, resolve: () => undefined }, + Finish: { target: targets1.root.Done, decoded: ({ state }) => (new Done({ value: state.value })) } } }, Done: { output: ({ state }) => state.value } @@ -87,6 +86,8 @@ describe("machine planner and runtime strategies", () => { it.effect("retains the selected named branch across generic and indexed-flat planning", () => { const states = Machine.state({ initial: "Count", states: { Count } }) const machine = Machine.make({ + branches: { transition1: { negative: { none: true }, zero: { none: true }, positive: { none: true } } }, + root: states, events: Machine.eventsFromSchemas(Select), initialConfiguration: (root) => @@ -95,18 +96,15 @@ describe("machine planner and runtime strategies", () => { states: { Count: { on: { - Select: (to) => - to.branches({ - negative: { target: to.none }, - zero: { target: to.none }, - positive: { target: to.none } - }).resolve(({ event, select }) => + Select: { + branches: "transition1", + resolve: ({ event, select }) => event.value < 0 ? select.negative() : event.value === 0 ? select.zero() : select.positive() - ) + } } } } @@ -131,7 +129,10 @@ describe("machine planner and runtime strategies", () => { it.effect("fails closed to generic planning for declinable transitions", () => { const states = Machine.state({ initial: "Count", states: { Count } }) + const targets3 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets3.root.Count } } }, + root: states, events: Machine.eventsFromSchemas(Select), initialConfiguration: (root) => @@ -140,11 +141,14 @@ describe("machine planner and runtime strategies", () => { states: { Count: { on: { - Select: (to) => - to.branch.Count().resolve(({ event, state, target, decline }) => + Select: { + branches: "transition1", + resolve: ({ event, state, select: { destination: target }, decline }) => event.value < 0 ? decline() - : target.decoded(new Count({ value: state.value + event.value })), { declinable: true }) + : target.decoded(new Count({ value: state.value + event.value })), + declinable: true + } } } } @@ -208,6 +212,7 @@ describe("machine planner and runtime strategies", () => { Outside } }) + const targets4 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(UpdateRegions, Compete, ExitRoot, ReenterUpdate), @@ -217,9 +222,12 @@ describe("machine planner and runtime strategies", () => { to.Root.decoded( new Root({ revision: 0 }), (root) => - root.Work.decoded(new Work({}), (work) => - work.Left.decoded(new Left({ value: 0 }), (left) => left.Leaf.decoded(new Leaf({}))) - .Right.decoded(new Right({ value: 0 }), (right) => right.Leaf.decoded(new Leaf({})))) + root.Work.decoded( + new Work({}), + (work) => + work.Left.decoded(new Left({ value: 0 }), (left) => left.Leaf.decoded(new Leaf({}))) + .Right.decoded(new Right({ value: 0 }), (right) => right.Leaf.decoded(new Leaf({}))) + ) ) ) ) @@ -233,18 +241,21 @@ describe("machine planner and runtime strategies", () => { states: { Leaf: { on: { - UpdateRegions: (to) => - to.local.update.resolve(({ current, owner }) => - owner.decoded(new Left({ value: current.value + 1 })) - ), - Compete: (to) => - to.branch.Root.update.resolve(({ owner }) => owner.decoded(new Root({ revision: 1 }))), - ExitRoot: (to) => - to.branch.Root.update.resolve(({ owner }) => owner.decoded(new Root({ revision: 3 }))), - ReenterUpdate: (to) => - to.local.update.reenter().resolve( - ({ current, owner }) => owner.decoded(new Left({ value: current.value + 1 })) - ) + UpdateRegions: { + update: targets4.root.Root.Work.Left, + decoded: ( + { ancestors: { "Root.Work.Left": current } } + ) => (new Left({ value: current.value + 1 })) + }, + Compete: { update: targets4.root.Root, decoded: () => (new Root({ revision: 1 })) }, + ExitRoot: { update: targets4.root.Root, decoded: () => (new Root({ revision: 3 })) }, + ReenterUpdate: { + update: targets4.root.Root.Work.Left, + reenter: true, + decoded: ( + { ancestors: { "Root.Work.Left": current } } + ) => (new Left({ value: current.value + 1 })) + } } } } @@ -253,13 +264,14 @@ describe("machine planner and runtime strategies", () => { states: { Leaf: { on: { - UpdateRegions: (to) => - to.local.update.resolve(({ current, owner }) => - owner.decoded(new Right({ value: current.value + 2 })) - ), - Compete: (to) => - to.branch.Root.update.resolve(({ owner }) => owner.decoded(new Root({ revision: 2 }))), - ExitRoot: (to) => to.branch.Outside().resolve(({ target }) => target.decoded(new Outside({}))) + UpdateRegions: { + update: targets4.root.Root.Work.Right, + decoded: ( + { ancestors: { "Root.Work.Right": current } } + ) => (new Right({ value: current.value + 2 })) + }, + Compete: { update: targets4.root.Root, decoded: () => (new Root({ revision: 2 })) }, + ExitRoot: { target: targets4.root.Outside, decoded: () => (new Outside({})) } } } } @@ -319,7 +331,10 @@ describe("machine planner and runtime strategies", () => { } } }) + const targets5 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets5.root.Ready.Saving, update: targets5.root.Ready } } }, + root: states, events: Machine.eventsFromSchemas(Save), initialConfiguration: (root) => @@ -332,14 +347,13 @@ describe("machine planner and runtime strategies", () => { states: { Idle: { on: { - Save: (to) => - to.local.Saving() - .updating(to.branch.Ready) - .resolve(({ current, event, owner, target }) => - target.decoded(new Saving({ request: event.request })).update( - owner.decoded(new Ready({ revision: current.revision + 1 })) - ) + Save: { + branches: "transition1", + resolve: ({ ancestors: { "Ready": current }, event, select: { destination: target } }) => + target.decoded(new Saving({ request: event.request })).update.decoded( + new Ready({ revision: current.revision + 1 }) ) + } } }, Saving: {} @@ -394,6 +408,7 @@ describe("machine planner and runtime strategies", () => { } } }) + const targets6 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Advance), @@ -412,18 +427,18 @@ describe("machine planner and runtime strategies", () => { states: { Left: { on: { - Advance: (to) => - to.branch.Root.Left().resolve(({ state, target }) => - target.decoded(new Left({ value: state.value + 1 })) - ) + Advance: { + target: targets6.root.Root.Left, + decoded: ({ state }) => (new Left({ value: state.value + 1 })) + } } }, Right: { on: { - Advance: (to) => - to.branch.Root.Right().resolve(({ state, target }) => - target.decoded(new Right({ value: state.value + 10 })) - ) + Advance: { + target: targets6.root.Root.Right, + decoded: ({ state }) => (new Right({ value: state.value + 10 })) + } } } } @@ -456,6 +471,7 @@ describe("machine planner and runtime strategies", () => { } } }) + const targets7 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Enter), @@ -465,7 +481,7 @@ describe("machine planner and runtime strategies", () => { states: { Outside: { on: { - Enter: (to) => to.branch.Opened.initial.resolve(({ target }) => target.decoded(new Opened({}))) + Enter: { initial: targets7.root.Opened, decoded: () => (new Opened({})) } } }, Opened: { @@ -539,6 +555,7 @@ describe("machine planner and runtime strategies", () => { class Idle extends Schema.TaggedClass("StrategyFallbackIdle")("Idle", {}) {} class Ready extends Schema.TaggedClass("StrategyFallbackReady")("Ready", {}) {} const states = Machine.state({ initial: "Idle", states: { Idle, Ready } }) + const targets8 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(), @@ -546,7 +563,7 @@ describe("machine planner and runtime strategies", () => { }).handle({ states: { Idle: { - always: (to) => to.branch.Ready().resolve(({ target }) => target.decoded(new Ready({}))) + always: { target: targets8.root.Ready, decoded: () => (new Ready({})) } }, Ready: {} } @@ -666,7 +683,10 @@ describe("machine planner and runtime strategies", () => { } }) const seen: Array = [] + const targets9 = Machine.targets(states) const definition = Machine.make({ + streams: { source1: Stream.suspend(() => Stream.fromIterable([1, 2, 3])) }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Streaming.from())) @@ -674,14 +694,17 @@ describe("machine planner and runtime strategies", () => { const machine = definition.handle({ states: { Streaming: { - invoke: (from) => - from.stream("values", () => Stream.fromIterable([1, 2, 3])).onElement((to) => - to.none.resolve(({ element }) => { + invoke: { + src: "source1", + id: "values", + onElement: { + none: true, + resolve: ({ element }) => { seen.push(element) - }) - ).onDone((to) => - to.branch.StreamDone().resolve(({ target }) => target.decoded(new StreamDone({ values: [...seen] }))) - ) + } + }, + onDone: { target: targets9.root.StreamDone, decoded: () => (new StreamDone({ values: [...seen] })) } + } }, StreamDone: { output: ({ state }) => state.values } } @@ -699,6 +722,7 @@ describe("machine planner and runtime strategies", () => { Effect.gen(function*() { const Event = Schema.TaggedUnion({ Set: { value: Schema.NonEmptyString } }) const states = Machine.state({ initial: "Count", states: { Count } }) + const targets10 = Machine.targets(states) const definition = Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), @@ -710,10 +734,7 @@ describe("machine planner and runtime strategies", () => { states: { Count: { on: { - Set: (to) => - to.branch.Count().resolve(({ event, target }) => - target.decoded(new Count({ value: event.value.length })) - ) + Set: { target: targets10.root.Count, decoded: ({ event }) => (new Count({ value: event.value.length })) } } } } @@ -763,12 +784,14 @@ describe("machine planner and runtime strategies", () => { states: { Idle: { on: { - Publish: (to) => - to.none.resolve(({ self }, enqueue) => { + Publish: { + none: true, + resolve: ({ self }, enqueue) => { assert.ok(self.sessionId.startsWith("machine:")) enqueue.emit(Emissions.Published({ value } as never)) return undefined - }) + } + } } } } @@ -934,7 +957,10 @@ describe("machine planner and runtime strategies", () => { Success: { schema: Success, type: "final", output: Schema.String } } }) + const targets12 = Machine.targets(states) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.succeed(new Loaded({ value: "complete" }))) }, + root: states, events: Machine.eventsFromSchemas(Load, Loaded), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.decoded(new Idle({})))) @@ -942,16 +968,18 @@ describe("machine planner and runtime strategies", () => { states: { Idle: { on: { - Load: (to) => to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({}))) + Load: { target: targets12.root.Loading, decoded: () => (new Loading({})) } } }, Loading: { - invoke: (from) => - from.effect("load", () => Effect.succeed(new Loaded({ value: "complete" }))).onDone((to) => - to.branch.Success().resolve(({ output, target }) => - target.decoded(new Success({ value: output.value })) - ) - ) + invoke: { + src: "source1", + id: "load", + onDone: { + target: targets12.root.Success, + decoded: ({ output }) => (new Success({ value: output.value })) + } + } }, Success: { output: ({ state }) => state.value } } @@ -1009,7 +1037,28 @@ describe("machine planner and runtime strategies", () => { for (const strategy of ["generic", "compiled"] as const) { let membershipAttempts = 0 let navigationRuns = 0 + const targets13 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { + authenticated: { target: targets13.root.Flow.Checking }, + anonymous: { target: targets13.root.Flow.Plans } + } + }, + effects: { + source1: Effect.suspend(() => + Effect.suspend(() => { + membershipAttempts += 1 + return membershipAttempts === 1 ? Effect.fail("offline") : Effect.succeed("active") + }) + ), + source2: Effect.suspend(() => + Effect.sync(() => { + navigationRuns += 1 + }).pipe(Effect.andThen(Effect.never)) + ) + }, + root: states, events: Machine.eventsFromSchemas(Retry), input: Schema.Struct({ authenticated: Schema.Boolean }), @@ -1020,33 +1069,27 @@ describe("machine planner and runtime strategies", () => { Flow: { states: { Routing: { - choice: (to) => - to.branches({ - authenticated: { target: to.local.Checking() }, - anonymous: { target: to.local.Plans() } - }).resolve(({ containingState, select }) => + choice: { + branches: "transition1", + resolve: ({ containingState, select }) => containingState.authenticated ? select.authenticated.from() : select.anonymous.from() - ) + } }, Checking: { - invoke: (from) => - from.effect("membership", () => - Effect.suspend(() => { - membershipAttempts += 1 - return membershipAttempts === 1 ? Effect.fail("offline") : Effect.succeed("active") - })).onDone((to) => to.local.MemberNavigating()).onFailure((to) => to.local.Failed()) + invoke: { + src: "source1", + id: "membership", + onDone: { target: targets13.root.Flow.MemberNavigating }, + onFailure: { target: targets13.root.Flow.Failed } + } }, Failed: { on: { - StrategyChoiceRetry: (to) => to.local.Checking() + StrategyChoiceRetry: { target: targets13.root.Flow.Checking } } }, MemberNavigating: { - invoke: (from) => - from.effect("navigate", () => - Effect.sync(() => { - navigationRuns += 1 - }).pipe(Effect.andThen(Effect.never))) + invoke: { src: "source2", id: "navigate" } } } } @@ -1086,7 +1129,10 @@ describe("machine planner and runtime strategies", () => { Failed: { schema: Failed, type: "final", output: Schema.String } } }) + const targets14 = Machine.targets(states) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.fail("unavailable")) }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -1094,10 +1140,11 @@ describe("machine planner and runtime strategies", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Effect.fail("unavailable")).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => target.decoded(new Failed({ error }))) - ) + invoke: { + src: "source1", + id: "load", + onFailure: { target: targets14.root.Failed, decoded: ({ error }) => (new Failed({ error })) } + } }, Failed: { output: ({ state }) => state.error } } @@ -1129,6 +1176,23 @@ describe("machine planner and runtime strategies", () => { const ParentEvents = Machine.eventsFromSchemas(ChildReady) const childStates = Machine.state({ initial: "ChildIdle", states: { ChildIdle } }) const childMachine = Machine.make({ + effects: { + source1: ({ + parent + }: Machine.Machine.InvokeContext< + { + readonly "": + & { readonly initial: "ChildIdle"; readonly states: { readonly ChildIdle: typeof ChildIdle } } + & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [], + readonly [], + "ChildIdle", + readonly [], + Machine.Machine.ParentEventSchemas<"required", readonly [typeof ChildReady]> + >) => parent.send(ParentEvents.ChildReady()) + }, + root: childStates, events: Machine.eventsFromSchemas(), parent: Machine.parent(ParentEvents), @@ -1137,11 +1201,13 @@ describe("machine planner and runtime strategies", () => { }).handle({ states: { ChildIdle: { - invoke: (from) => - from.effect("notify-parent", ({ parent }) => parent.send(ParentEvents.ChildReady())).onDone((to) => - to.none - ) - .onFailure((to) => to.none) + invoke: { + src: "source1", + id: "notify-parent", + input: (context) => context, + onDone: { none: true }, + onFailure: { none: true } + } } } }) @@ -1153,7 +1219,10 @@ describe("machine planner and runtime strategies", () => { ParentDone: { schema: ParentDone, type: "final", output: Schema.String } } }) + const targets16 = Machine.targets(parentStates) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: ParentEvents, initialConfiguration: (root) => @@ -1161,9 +1230,9 @@ describe("machine planner and runtime strategies", () => { }).handle({ states: { ParentWaiting: { - invoke: (from) => from.child(Child).onFailure((to) => to.none), + invoke: { src: "source1", onFailure: { none: true } }, on: { - ChildReady: (to) => to.branch.ParentDone().resolve(({ target }) => target.decoded(new ParentDone({}))) + ChildReady: { target: targets16.root.ParentDone, decoded: () => (new ParentDone({})) } } }, ParentDone: { output: () => "received" } @@ -1191,7 +1260,35 @@ describe("machine planner and runtime strategies", () => { const firstStarted = yield* Deferred.make() let generation = 0 const states = Machine.state({ initial: "Loading", states: { Loading, Failed } }) + const targets17 = Machine.targets(states) const definition = Machine.make({ + logic: { + worker: (_input: undefined) => { + generation += 1 + const current = generation + return Machine.logic({ + initial: "active", + run: ({ parent, sendTo, setState }) => + parent === undefined ? + Effect.die("worker expected an owning machine") : + (current === 1 ? Deferred.succeed(firstStarted, undefined) : Effect.void).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => + setState("stale").pipe( + Effect.andThen(sendTo(parent, new Stale({}))) + ) + ) + ) + }) + } + }, + branches: { + transition1: { + stale: { target: targets17.root.Failed, title: "Worker is stale" }, + unchanged: { none: true } + } + }, + root: states, events: Machine.eventsFromSchemas(Reenter, Stale), initialConfiguration: (root) => @@ -1200,43 +1297,25 @@ describe("machine planner and runtime strategies", () => { const machine = definition.handle({ states: { Loading: { - invoke: (from) => - from.logic("worker", { - address: Machine.childAddress("worker"), - logic: () => { - generation += 1 - const current = generation - return Machine.logic({ - initial: "active", - run: ({ parent, sendTo, setState }) => - parent === undefined ? - Effect.die("worker expected an owning machine") : - (current === 1 ? Deferred.succeed(firstStarted, undefined) : Effect.void).pipe( - Effect.andThen(Effect.never), - Effect.onInterrupt(() => - setState("stale").pipe( - Effect.andThen(sendTo(parent, new Stale({}))) - ) - ) - ) - }) - } - }).onFailure((to) => to.none).onSnapshot((to) => - to.branches({ - stale: { title: "Worker is stale", target: to.branch.Failed() }, - unchanged: { target: to.none } - }).resolve(({ snapshot, select }) => - snapshot.state === "stale" - ? select.stale.decoded(new Failed({})) - : select.unchanged() - ) - ), + invoke: { + src: "worker", + id: "worker", + address: Machine.childAddress("worker"), + input: () => undefined, + onFailure: { none: true }, + onSnapshot: { + branches: "transition1", + resolve: ({ snapshot, select }) => + snapshot.state === "stale" ? select.stale.decoded(new Failed({})) : select.unchanged() + } + }, on: { - Reenter: (to) => - to.branch.Loading().reenter().resolve( - ({ state, target }) => target.decoded(new Loading({ epoch: state.epoch + 1 })) - ), - Stale: (to) => to.branch.Failed().resolve(({ target }) => target.decoded(new Failed({}))) + Reenter: { + target: targets17.root.Loading, + reenter: true, + decoded: ({ state }) => (new Loading({ epoch: state.epoch + 1 })) + }, + Stale: { target: targets17.root.Failed, decoded: () => (new Failed({})) } } }, Failed: {} @@ -1283,18 +1362,41 @@ describe("machine planner and runtime strategies", () => { {} ) {} class Operating extends Schema.TaggedClass("StrategyDynamicOperating")("Operating", {}) {} + const root18 = Machine.state({ initial: "Commissioning", states: { Commissioning, Operating } }) + const targets18 = Machine.targets(root18) const machine = Machine.make({ - root: Machine.state({ initial: "Commissioning", states: { Commissioning, Operating } }), + effects: { + source1: ({ + children + }: Machine.Machine.InvokeContext< + { + readonly "": { + readonly initial: "Commissioning" + readonly states: { readonly Commissioning: typeof Commissioning; readonly Operating: typeof Operating } + } & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [], + readonly [], + "Commissioning", + readonly [], + readonly [] + >) => children.spawn(Child("runtime")) + }, + + root: root18, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Commissioning.decoded(new Commissioning({})))) }).handle({ states: { Commissioning: { - invoke: (from) => - from.effect("commission", ({ children }) => children.spawn(Child("runtime"))).onDone((to) => - to.branch.Operating() - ).onFailure((to) => to.none) + invoke: { + src: "source1", + id: "commission", + input: (context) => context, + onDone: { target: targets18.root.Operating }, + onFailure: { none: true } + } }, Operating: {} } diff --git a/packages/effect-machine/test/internal/machine/transitionConstructionStrategies.test.ts b/packages/effect-machine/test/internal/machine/transitionConstructionStrategies.test.ts index e7d7fc91..dba7f3fb 100644 --- a/packages/effect-machine/test/internal/machine/transitionConstructionStrategies.test.ts +++ b/packages/effect-machine/test/internal/machine/transitionConstructionStrategies.test.ts @@ -7,20 +7,28 @@ it.effect("compares flat updates and verifies guarded updates retain generic pla Effect.gen(function*() { for (const guarded of [false, true]) { const events = Machine.events({ Add: { by: Schema.Number }, Refresh: {} }) + const root1 = Machine.state({ fields: { count: Schema.Number } }) + const targets1 = Machine.targets(root1) const machine = Machine.make({ - root: Machine.state({ fields: { count: Schema.Number } }), + root: root1, events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ on: { - Add: (to) => - (guarded ? to.self.update.guard(({ event }) => event.by > 0) : to.self.update).from(( - { current, event } - ) => ({ count: current.count + event.by })), - Refresh: (to) => - to.none.reenter().resolve((_, enqueue) => { + Add: { + update: targets1.root, + guard: guarded + ? ({ event }) => event.by > 0 + : undefined, + from: ({ root, event }) => ({ count: root.count + event.by }) + }, + Refresh: { + none: true, + reenter: true, + resolve: (_, enqueue) => { enqueue.raise(events.Add({ by: 1 })) - }) + } + } } }) yield* verifyPlannerStrategies({ @@ -42,49 +50,58 @@ it.effect("compares atomic construction and verifies guards retain generic plann class Root extends Schema.TaggedClass("Root")("Root", { count: Schema.Number }) {} class Saved extends Schema.TaggedClass("Saved")("Saved", { text: Schema.String }) {} const events = Machine.events({ Save: { allowed: Schema.Boolean }, Decoded: {}, Branch: {}, Finish: {} }) + const root2 = Machine.state({ + schema: Root, + initial: "Idle", + states: { Idle: {}, Saved: { schema: Saved }, Done: { type: "final" } } + }) + const targets2 = Machine.targets(root2) const machine = Machine.make({ - root: Machine.state({ - schema: Root, - initial: "Idle", - states: { Idle: {}, Saved: { schema: Saved }, Done: { type: "final" } } - }), + branches: { transition1: { saved: { target: targets2.root.Saved } } }, + + root: root2, events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ - on: { Save: (to) => to.self.update.from(({ current }) => ({ count: current.count + 10 })) }, + on: { Save: { update: targets2.root, from: ({ root: current }) => ({ count: current.count + 10 }) } }, states: { Idle: { on: { - Save: (to) => { - const selected = to.local.Saved().updating(to.root) - return (guarded ? selected.guard(({ event }) => event.allowed) : selected).from(({ current }) => ({ - target: { text: "saved" }, - update: { count: current.count + 1 } - })) + Save: { + target: targets2.root.Saved, + update: targets2.root, + guard: guarded + ? ({ event }) => event.allowed + : undefined, + from: ({ root }) => ({ target: { text: "saved" }, update: { count: root.count + 1 } }) }, - Decoded: (to) => - to.local.Saved().updating(to.root).decoded(({ current }) => ({ + Decoded: { + target: targets2.root.Saved, + update: targets2.root, + decoded: ({ root: current }) => ({ target: new Saved({ text: "decoded" }), update: new Root({ count: current.count + 2 }) - })) + }) + } } }, Saved: { on: { - Save: (to) => { - const selected = to.local.Saved().updating(to.root).reenter() - return (guarded ? selected.guard(({ event }) => event.allowed) : selected).from(( - { current, state } - ) => ({ - target: { text: state.text }, - update: { count: current.count + 1 } - })) + Save: { + target: targets2.root.Saved, + update: targets2.root, + reenter: true, + guard: guarded + ? ({ event }) => event.allowed + : undefined, + from: ({ root, state }) => ({ target: { text: state.text }, update: { count: root.count + 1 } }) + }, + Branch: { + branches: "transition1", + reenter: true, + resolve: ({ state, select }) => select.saved.decoded(state) }, - Branch: (to) => - to.branches({ saved: { target: to.local.Saved() } }).reenter().resolve(({ state, select }) => - select.saved.decoded(state) - ), - Finish: (to) => to.local.Done() + Finish: { target: targets2.root.Done } } } } diff --git a/packages/effect-machine/test/machine/ActivityLifecycleModel.test.ts b/packages/effect-machine/test/machine/ActivityLifecycleModel.test.ts index 1bce15f6..f7dad00c 100644 --- a/packages/effect-machine/test/machine/ActivityLifecycleModel.test.ts +++ b/packages/effect-machine/test/machine/ActivityLifecycleModel.test.ts @@ -70,7 +70,10 @@ describe("machine activity lifecycle model", () => { Effect.gen(function*() { const probe = yield* makeActivityProbe const states = Machine.state({ initial: "Idle", states: { Idle, Active } }) + const targets1 = Machine.targets(states) const machine = Machine.make({ + logic: { source1: probe.logic("active", { _tag: "Blocked" }) }, + root: states, events: Machine.eventsFromSchemas(Enter, Leave, Restart), initialConfiguration: (root) => @@ -79,19 +82,20 @@ describe("machine activity lifecycle model", () => { states: { Idle: { on: { - Enter: (to) => to.branch.Active().resolve(({ target }) => target.decoded(new Active({}))) + Enter: { target: targets1.root.Active, decoded: () => (new Active({})) } } }, Active: { - invoke: (from) => - from.logic("activity", { - address: Machine.childAddress("activity"), - logic: probe.logic("active", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none), + invoke: { + src: "source1", + id: "activity", + address: Machine.childAddress("activity"), + onDone: { none: true }, + onFailure: { none: true } + }, on: { - Leave: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({}))), - Restart: (to) => - to.branch.Active().reenter().resolve(({ target }) => target.decoded(new Active({}))) + Leave: { target: targets1.root.Idle, decoded: () => (new Idle({})) }, + Restart: { target: targets1.root.Active, reenter: true, decoded: () => (new Active({})) } } } } @@ -146,7 +150,10 @@ describe("machine activity lifecycle model", () => { Done: { schema: Done, type: "final", output: Schema.Number } } }) + const targets2 = Machine.targets(states) const machine = Machine.make({ + logic: { source1: probe.immediate("immediate", (epoch) => new Completed({ epoch })) }, + root: states, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(Completed), @@ -155,14 +162,13 @@ describe("machine activity lifecycle model", () => { }).handle({ states: { Active: { - invoke: (from) => - from.logic("immediate", { - address: Machine.childAddress("immediate"), - logic: probe.immediate("immediate", (epoch) => new Completed({ epoch })) - }).onDone((to) => - to.branch.Done().resolve(({ output, target }) => target.decoded(new Done({ epoch: output.epoch }))) - ) - .onFailure((to) => to.none) + invoke: { + src: "source1", + id: "immediate", + address: Machine.childAddress("immediate"), + onDone: { target: targets2.root.Done, decoded: ({ output }) => (new Done({ epoch: output.epoch })) }, + onFailure: { none: true } + } }, Done: { output: ({ state }) => state.epoch @@ -186,7 +192,15 @@ describe("machine activity lifecycle model", () => { acknowledged: Schema.Number }) {} const states = Machine.state({ initial: "Active", states: { Active: EpochActive, Done } }) + const targets3 = Machine.targets(states) const machine = Machine.make({ + logic: { + source1: probe.logic("epoch", { + _tag: "StaleOnCancel", + event: (epoch) => new Completed({ epoch }) + }) + }, + root: states, events: Machine.eventsFromSchemas(Restart, QueueBarrier), internalEvents: Machine.internalEventsFromSchemas(Completed), @@ -195,25 +209,24 @@ describe("machine activity lifecycle model", () => { }).handle({ states: { Active: { - invoke: (from) => - from.logic("epoch", { - address: Machine.childAddress("epoch"), - logic: probe.logic("epoch", { - _tag: "StaleOnCancel", - event: (epoch) => new Completed({ epoch }) - }) - }).onDone((to) => to.none).onFailure((to) => to.none), + invoke: { + src: "source1", + id: "epoch", + address: Machine.childAddress("epoch"), + onDone: { none: true }, + onFailure: { none: true } + }, on: { - Restart: (to) => - to.branch.Active().reenter().resolve( - ({ state, target }) => target.decoded(new EpochActive({ acknowledged: state.acknowledged })) - ), - QueueBarrier: (to) => - to.branch.Active().resolve(({ state, target }) => - target.decoded(new EpochActive({ acknowledged: state.acknowledged + 1 })) - ), - Completed: (to) => - to.branch.Done().resolve(({ event, target }) => target.decoded(new Done({ epoch: event.epoch }))) + Restart: { + target: targets3.root.Active, + reenter: true, + decoded: ({ state }) => (new EpochActive({ acknowledged: state.acknowledged })) + }, + QueueBarrier: { + target: targets3.root.Active, + decoded: ({ state }) => (new EpochActive({ acknowledged: state.acknowledged + 1 })) + }, + Completed: { target: targets3.root.Done, decoded: ({ event }) => (new Done({ epoch: event.epoch })) } } }, Done: {} @@ -270,28 +283,35 @@ describe("machine activity lifecycle model", () => { class Right extends Schema.TaggedClass("ActivityRight")("Right", {}) {} class RightActive extends Schema.TaggedClass("ActivityRightActive")("RightActive", {}) {} class LeaveLeft extends Schema.TaggedClass("ActivityLeaveLeft")("LeaveLeft", {}) {} - const machine = Machine.make({ - root: Machine.state({ - initial: "Root", - states: { - Root: { - schema: Root, - type: "parallel", - states: { - left: { - schema: Left, - initial: "active", - states: { active: LeftActive, idle: LeftIdle } - }, - right: { - schema: Right, - initial: "active", - states: { active: RightActive } - } + const root4 = Machine.state({ + initial: "Root", + states: { + Root: { + schema: Root, + type: "parallel", + states: { + left: { + schema: Left, + initial: "active", + states: { active: LeftActive, idle: LeftIdle } + }, + right: { + schema: Right, + initial: "active", + states: { active: RightActive } } } } - }), + } + }) + const targets4 = Machine.targets(root4) + const machine = Machine.make({ + logic: { + source1: probe.logic("left", { _tag: "Blocked" }), + source2: probe.logic("right", { _tag: "Blocked" }) + }, + + root: root4, events: Machine.eventsFromSchemas(LeaveLeft), initialConfiguration: (to) => to.resolve(() => ({ @@ -321,13 +341,15 @@ describe("machine activity lifecycle model", () => { left: { states: { active: { - invoke: (from) => - from.logic("left-activity", { - address: Machine.childAddress("left-activity"), - logic: probe.logic("left", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none), + invoke: { + src: "source1", + id: "left-activity", + address: Machine.childAddress("left-activity"), + onDone: { none: true }, + onFailure: { none: true } + }, on: { - LeaveLeft: (to) => to.local.idle().resolve(({ target }) => target.decoded(new LeftIdle({}))) + LeaveLeft: { target: targets4.root.Root.left.idle, decoded: () => (new LeftIdle({})) } } } } @@ -335,11 +357,13 @@ describe("machine activity lifecycle model", () => { right: { states: { active: { - invoke: (from) => - from.logic("right-activity", { - address: Machine.childAddress("right-activity"), - logic: probe.logic("right", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none) + invoke: { + src: "source2", + id: "right-activity", + address: Machine.childAddress("right-activity"), + onDone: { none: true }, + onFailure: { none: true } + } } } } @@ -375,7 +399,11 @@ describe("machine activity lifecycle model", () => { Effect.gen(function*() { const probe = yield* makeActivityProbe const states = Machine.state({ initial: "Idle", states: { Idle, Active, Done } }) + const targets5 = Machine.targets(states) const machine = Machine.make({ + timers: { source2: "1 hour" }, + logic: { source1: probe.logic("timed", { _tag: "Blocked" }) }, + root: states, events: Machine.eventsFromSchemas(Leave), internalEvents: Machine.internalEventsFromSchemas(TimerFired), @@ -385,19 +413,19 @@ describe("machine activity lifecycle model", () => { states: { Idle: {}, Active: { - invoke: ( - from - ) => [ - from.logic("timed-activity", { - address: Machine.childAddress("timed-activity"), - logic: probe.logic("timed", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none), - from.timer("deadline", "1 hour").onDone((to) => - to.branch.Done().resolve(({ target }) => target.decoded(new Done({ epoch: -1 }))) - ) - ], + invoke: [{ + src: "source1", + id: "timed-activity", + address: Machine.childAddress("timed-activity"), + onDone: { none: true }, + onFailure: { none: true } + }, { + src: "source2", + id: "deadline", + onDone: { target: targets5.root.Done, decoded: () => (new Done({ epoch: -1 })) } + }], on: { - Leave: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({}))) + Leave: { target: targets5.root.Idle, decoded: () => (new Idle({})) } } }, Done: {} @@ -423,6 +451,11 @@ describe("machine activity lifecycle model", () => { const probe = yield* makeActivityProbe const states = Machine.state({ initial: "Active", states: { Active } }) const machine = Machine.make({ + logic: { + source1: probe.logic("failing", { _tag: "Failure" }), + source2: probe.logic("sibling", { _tag: "Blocked" }) + }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -430,22 +463,24 @@ describe("machine activity lifecycle model", () => { }).handle({ states: { Active: { - invoke: ( - from - ) => [ - from.logic("failing", { - address: Machine.childAddress("failing"), - logic: probe.logic("failing", { _tag: "Failure" }) - }).onDone((to) => to.none).onFailure((to) => - to.none.resolve(({ error }) => { + invoke: [{ + src: "source1", + id: "failing", + address: Machine.childAddress("failing"), + onDone: { none: true }, + onFailure: { + none: true, + resolve: ({ error }) => { throw error - }) - ), - from.logic("sibling", { - address: Machine.childAddress("sibling"), - logic: probe.logic("sibling", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none) - ] + } + } + }, { + src: "source2", + id: "sibling", + address: Machine.childAddress("sibling"), + onDone: { none: true }, + onFailure: { none: true } + }] } } }) @@ -472,6 +507,11 @@ describe("machine activity lifecycle model", () => { const probe = yield* makeActivityProbe const states = Machine.state({ initial: "Active", states: { Active } }) const machine = Machine.make({ + logic: { + source1: probe.logic("first", { _tag: "Blocked" }), + source2: probe.logic("second", { _tag: "Blocked" }) + }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -479,18 +519,19 @@ describe("machine activity lifecycle model", () => { }).handle({ states: { Active: { - invoke: ( - from - ) => [ - from.logic("first", { - address: Machine.childAddress("first"), - logic: probe.logic("first", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none), - from.logic("second", { - address: Machine.childAddress("second"), - logic: probe.logic("second", { _tag: "Blocked" }) - }).onDone((to) => to.none).onFailure((to) => to.none) - ] + invoke: [{ + src: "source1", + id: "first", + address: Machine.childAddress("first"), + onDone: { none: true }, + onFailure: { none: true } + }, { + src: "source2", + id: "second", + address: Machine.childAddress("second"), + onDone: { none: true }, + onFailure: { none: true } + }] } } }) diff --git a/packages/effect-machine/test/machine/Choice.test.ts b/packages/effect-machine/test/machine/Choice.test.ts index 0db4798b..15e7b43c 100644 --- a/packages/effect-machine/test/machine/Choice.test.ts +++ b/packages/effect-machine/test/machine/Choice.test.ts @@ -23,9 +23,27 @@ const States = Machine.state({ } }) -let branchFactoryCalls = 0 +let branchDeclarationReads = 0 +const targets1 = Machine.targets(States) const machine = Machine.make({ + branches: { + routing: { + perfect: { + get title() { + branchDeclarationReads++ + return "Score is perfect" + }, + target: targets1.root.Flow.Approved + }, + negative: { title: "Score is negative", target: targets1.root.Flow.Rejected }, + zero: { title: "Score is zero", target: targets1.root.Flow.Rejected }, + passing: { title: "Score is at least 70", target: targets1.root.Flow.Approved }, + failing: { target: targets1.root.Flow.Rejected } + }, + transition1: { destination: { initial: targets1.root.Flow } } + }, + root: States, events: Machine.eventsFromSchemas(Recheck), initialConfiguration: (root) => @@ -37,15 +55,9 @@ const machine = Machine.make({ Flow: { states: { Routing: { - choice: (to) => { - branchFactoryCalls += 1 - return to.branches({ - perfect: { title: "Score is perfect", target: to.local.Approved() }, - negative: { title: "Score is negative", target: to.local.Rejected() }, - zero: { title: "Score is zero", target: to.local.Rejected() }, - passing: { title: "Score is at least 70", target: to.local.Approved() }, - failing: { target: to.local.Rejected() } - }).resolve(({ containingState, select }) => { + choice: { + branches: "routing", + resolve: ({ containingState, select }) => { const score = containingState.score return score === 100 ? select.perfect.decoded(new Approved({})) @@ -56,13 +68,12 @@ const machine = Machine.make({ : score >= 70 ? select.passing.decoded(new Approved({})) : select.failing.decoded(new Rejected({})) - }) + } } }, Approved: { on: { - Recheck: (to) => - to.branch.Flow.initial.resolve(({ event, target }) => target.decoded(new Flow({ score: event.score }))) + Recheck: { initial: targets1.root.Flow, decoded: ({ event }) => (new Flow({ score: event.score })) } } } } @@ -85,7 +96,7 @@ describe("Machine choice pseudo-states", () => { assert.strictEqual(plan.microsteps[0]?.transitions[0]?.branchIndex, 3) assert.strictEqual(plan.microsteps[0]?.transitions[0]?.branchKey, "passing") assert.strictEqual(Machine.isInitialEvent(plan.microsteps[0]!.event), true) - assert.strictEqual(branchFactoryCalls, 1) + assert.strictEqual(branchDeclarationReads, 1) const choice = Machine.transitionDefinitions(machine).find(({ source }) => source === "Flow.Routing") assert.deepStrictEqual(choice?.branches, [ { @@ -93,7 +104,7 @@ describe("Machine choice pseudo-states", () => { key: "perfect", title: "Score is perfect", target: "Flow.Approved", - selection: { path: "Flow.Approved", kind: "state", scope: "local" }, + selection: { path: "Flow.Approved", kind: "state", scope: "branch" }, updates: [] }, { @@ -101,7 +112,7 @@ describe("Machine choice pseudo-states", () => { key: "negative", title: "Score is negative", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] }, { @@ -109,7 +120,7 @@ describe("Machine choice pseudo-states", () => { key: "zero", title: "Score is zero", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] }, { @@ -117,7 +128,7 @@ describe("Machine choice pseudo-states", () => { key: "passing", title: "Score is at least 70", target: "Flow.Approved", - selection: { path: "Flow.Approved", kind: "state", scope: "local" }, + selection: { path: "Flow.Approved", kind: "state", scope: "branch" }, updates: [] }, { @@ -125,7 +136,7 @@ describe("Machine choice pseudo-states", () => { key: "failing", title: "failing", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] } ]) @@ -167,15 +178,25 @@ describe("Machine choice pseudo-states", () => { } }) let observedContext: { readonly enabled: boolean; readonly score: number } | undefined + const targets2 = Machine.targets(states) const inputChoice = Machine.make({ + branches: { + transition1: { + approved: { target: targets2.root.Root.Flow.Approved }, + rejected: { target: targets2.root.Root.Flow.Rejected } + } + }, + root: states, events: Machine.eventsFromSchemas(), input: Schema.Struct({ enabled: Schema.Boolean, score: Schema.Number }), initialConfiguration: (root) => root.resolve(({ input, target }) => target.from((to) => - to.Root.from({ enabled: input.enabled }, (root) => - root.Flow.from({ score: input.score }, (flow) => flow.Routing())) + to.Root.from( + { enabled: input.enabled }, + (root) => root.Flow.from({ score: input.score }, (flow) => flow.Routing()) + ) ) ) }).handle({ @@ -185,11 +206,9 @@ describe("Machine choice pseudo-states", () => { Flow: { states: { Routing: { - choice: (to) => - to.branches({ - approved: { target: to.local.Approved() }, - rejected: { target: to.local.Rejected() } - }).resolve(({ ancestors, containingState, select }) => { + choice: { + branches: "transition1", + resolve: ({ ancestors, containingState, select }) => { observedContext = { enabled: ancestors.Root.enabled, score: containingState.score @@ -197,7 +216,8 @@ describe("Machine choice pseudo-states", () => { return ancestors.Root.enabled && containingState.score >= 70 ? select.approved.decoded(new Approved({})) : select.rejected.decoded(new Rejected({})) - }) + } + } } } } @@ -240,7 +260,10 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets3 = Machine.targets(states) const chained = Machine.make({ + branches: { transition1: { destination: { target: targets3.root.Flow.Second } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -252,10 +275,10 @@ describe("Machine choice pseudo-states", () => { Flow: { states: { First: { - choice: (to) => to.local.Second().resolve(({ target }) => target()) + choice: { branches: "transition1", resolve: ({ select: { destination: target } }) => target() } }, Second: { - choice: (to) => to.local.Approved().resolve(({ target }) => target.decoded(new Approved({}))) + choice: { target: targets3.root.Flow.Approved, decoded: () => (new Approved({})) } } } } @@ -294,7 +317,13 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets4 = Machine.targets(states) const looping = Machine.make({ + branches: { + transition1: { destination: { target: targets4.root.Flow.Second } }, + transition2: { destination: { target: targets4.root.Flow.First } } + }, + id: "ChoiceLoopMachine", root: states, events: Machine.eventsFromSchemas(), @@ -307,10 +336,10 @@ describe("Machine choice pseudo-states", () => { Flow: { states: { First: { - choice: (to) => to.local.Second().resolve(({ target }) => target()) + choice: { branches: "transition1", resolve: ({ select: { destination: target } }) => target() } }, Second: { - choice: (to) => to.local.First().resolve(({ target }) => target()) + choice: { branches: "transition2", resolve: ({ select: { destination: target } }) => target() } } } } @@ -339,7 +368,10 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets5 = Machine.targets(states) const alwaysMachine = Machine.make({ + branches: { transition1: { destination: { target: targets5.root.Flow.Routing } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -353,10 +385,10 @@ describe("Machine choice pseudo-states", () => { Flow: { states: { Approved: { - always: (to) => to.local.Routing().resolve(({ target }) => target()) + always: { branches: "transition1", resolve: ({ select: { destination: target } }) => target() } }, Routing: { - choice: (to) => to.local.Rejected().resolve(({ target }) => target.decoded(new Rejected({}))) + choice: { target: targets5.root.Flow.Rejected, decoded: () => (new Rejected({})) } } } } @@ -400,7 +432,10 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets6 = Machine.targets(states) const completion = Machine.make({ + branches: { transition1: { destination: { target: targets6.root.Flow } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -410,11 +445,13 @@ describe("Machine choice pseudo-states", () => { }).handle({ states: { Flow: { - onDone: (to) => - to.branch.Flow().resolve(({ state, target }) => target.decoded(state, (flow) => flow.Routing())), + onDone: { + branches: "transition1", + resolve: ({ state, select: { destination: target } }) => target.decoded(state, (flow) => flow.Routing()) + }, states: { Routing: { - choice: (to) => to.local.Rejected().resolve(({ target }) => target.decoded(new Rejected({}))) + choice: { target: targets6.root.Flow.Rejected, decoded: () => (new Rejected({})) } } } } @@ -466,6 +503,7 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets7 = Machine.targets(states) const parallel = Machine.make({ root: states, events: Machine.eventsFromSchemas(), @@ -485,14 +523,14 @@ describe("Machine choice pseudo-states", () => { Left: { states: { Routing: { - choice: (to) => to.local.Ready().resolve(({ target }) => target.decoded(new Ready({}))) + choice: { target: targets7.root.Board.Left.Ready, decoded: () => (new Ready({})) } } } }, Right: { states: { Routing: { - choice: (to) => to.local.Ready().resolve(({ target }) => target.decoded(new RightReady({}))) + choice: { target: targets7.root.Board.Right.Ready, decoded: () => (new RightReady({})) } } } } @@ -533,7 +571,13 @@ describe("Machine choice pseudo-states", () => { Outside } }) + const targets8 = Machine.targets(states) const history = Machine.make({ + branches: { + transition2: { destination: { history: targets8.root.Flow.Recent } }, + transition3: { destination: { target: targets8.root.Flow } } + }, + root: states, events: Machine.eventsFromSchemas(Leave, Resume), initialConfiguration: (root) => @@ -554,20 +598,21 @@ describe("Machine choice pseudo-states", () => { states: { Active: { on: { - Leave: (to) => to.branch.Outside().resolve(({ target }) => target.decoded(new Outside({}))) + Leave: { target: targets8.root.Outside, decoded: () => (new Outside({})) } } }, Routing: { - choice: (to) => to.history.Flow.Recent.resolve(({ target }) => target()) + choice: { branches: "transition2", resolve: ({ select: { destination: target } }) => target() } } } }, Outside: { on: { - Resume: (to) => - to.branch.Flow().resolve(({ target }) => + Resume: { + branches: "transition3", + resolve: ({ select: { destination: target } }) => target.decoded(new Flow({ score: 2 }), (flow) => flow.Routing()) - ) + } } } } @@ -600,7 +645,10 @@ describe("Machine choice pseudo-states", () => { } } }) + const targets9 = Machine.targets(states) const initialHistory = Machine.make({ + branches: { transition1: { destination: { history: targets9.root.Flow.Recent } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -625,7 +673,7 @@ describe("Machine choice pseudo-states", () => { }, states: { Routing: { - choice: (to) => to.history.Flow.Recent.resolve(({ target }) => target()) + choice: { branches: "transition1", resolve: ({ select: { destination: target } }) => target() } } } } @@ -658,7 +706,10 @@ describe("Machine choice pseudo-states", () => { Outside } }) + const targets10 = Machine.targets(states) const historyChoice = Machine.make({ + branches: { transition2: { destination: { history: targets10.root.Flow.Recent } } }, + root: states, events: Machine.eventsFromSchemas(Resume), initialConfiguration: (root) => @@ -674,13 +725,16 @@ describe("Machine choice pseudo-states", () => { }, states: { Routing: { - choice: (to) => to.local.Active().resolve(({ target }) => target.decoded(new Active({}))) + choice: { target: targets10.root.Flow.Active, decoded: () => (new Active({})) } } } }, Outside: { on: { - FallbackChoiceResume: (to) => to.history.Flow.Recent.resolve(({ target }) => target()) + FallbackChoiceResume: { + branches: "transition2", + resolve: ({ select: { destination: target } }) => target() + } } } } @@ -722,7 +776,7 @@ describe("Machine choice pseudo-states", () => { key: "perfect", title: "Score is perfect", target: "Flow.Approved", - selection: { path: "Flow.Approved", kind: "state", scope: "local" }, + selection: { path: "Flow.Approved", kind: "state", scope: "branch" }, updates: [] }, { @@ -730,7 +784,7 @@ describe("Machine choice pseudo-states", () => { key: "negative", title: "Score is negative", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] }, { @@ -738,7 +792,7 @@ describe("Machine choice pseudo-states", () => { key: "zero", title: "Score is zero", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] }, { @@ -746,7 +800,7 @@ describe("Machine choice pseudo-states", () => { key: "passing", title: "Score is at least 70", target: "Flow.Approved", - selection: { path: "Flow.Approved", kind: "state", scope: "local" }, + selection: { path: "Flow.Approved", kind: "state", scope: "branch" }, updates: [] }, { @@ -754,7 +808,7 @@ describe("Machine choice pseudo-states", () => { key: "failing", title: "failing", target: "Flow.Rejected", - selection: { path: "Flow.Rejected", kind: "state", scope: "local" }, + selection: { path: "Flow.Rejected", kind: "state", scope: "branch" }, updates: [] } ] diff --git a/packages/effect-machine/test/machine/Declarative.test.ts b/packages/effect-machine/test/machine/Declarative.test.ts new file mode 100644 index 00000000..a3c7f004 --- /dev/null +++ b/packages/effect-machine/test/machine/Declarative.test.ts @@ -0,0 +1,208 @@ +import { assert, describe, it } from "@effect/vitest" +import { Context, Effect, Schema } from "effect" +import { Machine } from "../../src/index.js" + +const Root = Machine.state({ + initial: "Idle", + states: { + Idle: {}, + Ready: Schema.TaggedStruct("Ready", { count: Schema.Number }), + Checkout: { + schema: Schema.TaggedStruct("Checkout", { orderId: Schema.String }), + initial: "Review", + states: { Review: Schema.TaggedStruct("Review", { total: Schema.Number }) } + } + } +}) +const targets = Machine.targets(Root) +const events = Machine.events({ Complete: { count: Schema.Number } }) + +describe("declarative transitions", () => { + it.effect("accepts a service class as a direct lazy Effect", () => + Effect.gen(function*() { + class Value extends Context.Service()("test/direct/Value") {} + const root = Machine.state({ + initial: "Loading", + states: { + Loading: {}, + Done: { type: "final", fields: { count: Schema.Number }, output: Schema.Number } + } + }) + const refs = Machine.targets(root) + const machine = Machine.make({ root, events: Machine.events({}), effects: { value: Value } }).handle({ + states: { + Loading: { invoke: { src: "value", onDone: { target: refs.root.Done, from: ({ output }) => output } } }, + Done: { output: ({ state }) => state.count } + } + }) + const count = yield* Effect.gen(function*() { + const ref = yield* Machine.start(machine) + return yield* ref.join + }).pipe(Effect.provideService(Value, { count: 7 })) + assert.strictEqual(count, 7) + })) + + it.effect("captures registered values before caller-owned registries change", () => + Effect.gen(function*() { + const root = Machine.state({ + initial: "Loading", + states: { + Loading: {}, + Done: { type: "final", fields: { value: Schema.Number }, output: Schema.Number } + } + }) + const refs = Machine.targets(root) + const effects = { load: Effect.succeed(1) } + const branches = { finish: { done: { target: refs.root.Done } } } + const definition = Machine.make({ root, events: Machine.events({}), effects, branches }) + effects.load = Effect.succeed(99) + const machine = definition.handle({ + states: { + Loading: { + invoke: { + src: "load", + onDone: { branches: "finish", resolve: ({ output, select }) => select.done.from({ value: output }) } + } + }, + Done: { output: ({ state }) => state.value } + } + }) + const ref = yield* Machine.start(machine) + assert.strictEqual(yield* ref.join, 1) + })) + + it.effect("allows inline entry into a choice with inspectable destinations", () => + Effect.gen(function*() { + const root = Machine.state({ initial: "Idle", states: { Idle: {}, Route: { type: "choice" }, Ready: {} } }) + const refs = Machine.targets(root) + const machine = Machine.make({ root, events: Machine.events({ Go: {} }) }).handle({ + states: { + Idle: { on: { Go: { target: refs.root.Route } } }, + Route: { choice: { target: refs.root.Ready } } + } + }) + const initial = yield* Machine.planInitial(machine) + const next = yield* Machine.plan(machine, initial.state, { _tag: "Go" }) + assert.isTrue(root.matches(next.next, "Ready")) + assert.strictEqual(Machine.transitionDefinitions(machine)[0]?.branches[0]?.target, "Route") + })) + + it.effect("captures inline topology without evaluating construction", () => + Effect.gen(function*() { + let calls = 0 + const machine = Machine.make({ root: Root, events }).handle({ + states: { + Idle: { + on: { + Complete: { + target: targets.root.Ready, + from: ({ event }) => { + calls++ + return { count: event.count } + } + } + } + }, + Ready: {}, + Checkout: { states: { Review: {} } } + } + }) + assert.strictEqual(calls, 0) + assert.strictEqual(Machine.transitionDefinitions(machine)[0]?.branches[0]?.target, "Ready") + const initial = yield* Machine.planInitial(machine) + const plan = yield* Machine.plan(machine, initial.state, { _tag: "Complete", count: 3 }) + assert.strictEqual(calls, 1) + assert.deepStrictEqual(plan.next, { + path: "", + value: undefined, + state: { path: "Ready", value: { _tag: "Ready", count: 3 } } + }) + })) + + it.effect("captures branch topology and constructs nested values lazily", () => + Effect.gen(function*() { + let calls = 0 + const machine = Machine.make({ + root: Root, + events, + branches: { + checkout: { review: { target: targets.root.Checkout } } + } + }).handle({ + states: { + Idle: { + on: { + Complete: { + branches: "checkout", + resolve: ({ event, select }) => { + calls++ + return select.review.from( + { orderId: "order-1" }, + (child) => child.Review.from({ total: event.count }) + ) + } + } + } + }, + Ready: {}, + Checkout: { states: { Review: {} } } + } + }) + assert.strictEqual(calls, 0) + assert.strictEqual(Machine.transitionDefinitions(machine)[0]?.branches[0]?.target, "Checkout") + const initial = yield* Machine.planInitial(machine) + const plan = yield* Machine.plan(machine, initial.state, { _tag: "Complete", count: 4 }) + assert.strictEqual(calls, 1) + assert.deepStrictEqual(plan.next, { + path: "", + value: undefined, + state: { + path: "Checkout", + value: { _tag: "Checkout", orderId: "order-1" }, + state: { + path: "Checkout.Review", + value: { _tag: "Review", total: 4 } + } + } + }) + })) + + it.effect("runs registered input-taking Effects with provided services", () => + Effect.gen(function*() { + class Database extends Context.Service()("test/declarative/Database") {} + const root = Machine.state({ + initial: "Idle", + states: { + Idle: {}, + Done: { type: "final", schema: Schema.TaggedStruct("Done", { count: Schema.Number }), output: Schema.Number } + } + }) + const refs = Machine.targets(root) + const machine = Machine.make({ + root, + events: Machine.events({}), + effects: { + load: (offset: number) => Effect.map(Database, (db) => db.count + offset) + } + }).handle({ + states: { + Idle: { + invoke: { + src: "load", + input: () => 2, + onDone: { + target: refs.root.Done, + from: ({ output }) => ({ count: output }) + } + } + }, + Done: { output: ({ state }) => state.count } + } + }) + const value = yield* Effect.gen(function*() { + const ref = yield* Machine.start(machine) + return yield* ref.join + }).pipe(Effect.provideService(Database, { count: 5 })) + assert.strictEqual(value, 7) + })) +}) diff --git a/packages/effect-machine/test/machine/DeepHandlers.test.ts b/packages/effect-machine/test/machine/DeepHandlers.test.ts index d8053360..d1a6860a 100644 --- a/packages/effect-machine/test/machine/DeepHandlers.test.ts +++ b/packages/effect-machine/test/machine/DeepHandlers.test.ts @@ -105,6 +105,7 @@ const initial = (() => { return { path: "", value: undefined, state } as Machine.Snapshot })() +const targets1 = Machine.targets(States) const machine = Machine.make({ root: States, events: Machine.eventsFromSchemas(Advance), @@ -137,10 +138,13 @@ const machine = Machine.make({ states: { idle: { on: { - Advance: (to) => - to.local.done().resolve(({ event, target }) => - target.decoded(new DeepDone({ value: event.value })) - ) + Advance: { + target: + targets1.root.n0.n1.n2.n3.n4.n5.n6.n7.n8.n9.n10.n11.done, + decoded: ( + { event } + ) => (new DeepDone({ value: event.value })) + } } }, done: {} diff --git a/packages/effect-machine/test/machine/DynamicChildren.test.ts b/packages/effect-machine/test/machine/DynamicChildren.test.ts index 168eeac7..17dd5a81 100644 --- a/packages/effect-machine/test/machine/DynamicChildren.test.ts +++ b/packages/effect-machine/test/machine/DynamicChildren.test.ts @@ -35,6 +35,7 @@ describe("dynamic child machines", () => { const PlantInput = Schema.Struct({ id: Schema.String, production: Schema.Number }) const PlantOwnerEvents = Machine.eventsFromSchemas(PlantReported) const plantStates = Machine.state({ initial: "PlantActive", states: { PlantActive } }) + const targets1 = Machine.targets(plantStates) const plantMachine = Machine.make({ root: plantStates, events: Machine.eventsFromSchemas(Produce, Report), @@ -48,14 +49,16 @@ describe("dynamic child machines", () => { states: { PlantActive: { on: { - Produce: (to) => - to.branch.PlantActive().resolve(({ event, state, target }) => - target.decoded(new PlantActive({ ...state, produced: state.produced + event.amount })) - ), - Report: (to) => - to.none.resolve(({ parent, state }, enqueue) => { + Produce: { + target: targets1.root.PlantActive, + decoded: ({ event, state }) => (new PlantActive({ ...state, produced: state.produced + event.amount })) + }, + Report: { + none: true, + resolve: ({ parent, state }, enqueue) => { enqueue.sendTo(parent, PlantOwnerEvents.PlantReported({ id: state.id, produced: state.produced })) - }) + } + } } } } @@ -76,7 +79,32 @@ describe("dynamic child machines", () => { id: Schema.String }) {} const parentStates = Machine.state({ initial: "Commissioning", states: { Commissioning, Operating } }) + const targets2 = Machine.targets(parentStates) const parentMachine = Machine.make({ + effects: { + source1: ({ + children, + state + }: Machine.Machine.InvokeContext< + { + readonly "": { + readonly initial: "Commissioning" + readonly states: { readonly Commissioning: typeof Commissioning; readonly Operating: typeof Operating } + } & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [typeof PlantReported, typeof Grow, typeof Decommission], + readonly [], + "Commissioning", + readonly [typeof PlantReported, typeof Grow, typeof Decommission], + readonly [] + >) => + Effect.forEach( + state.plants, + (input) => children.spawn(Plant(input.id), { input }), + { discard: true } + ) + }, + root: parentStates, events: Machine.eventsFromSchemas(PlantOwnerEvents, Grow, Decommission), input: Schema.Array(PlantInput), @@ -87,31 +115,30 @@ describe("dynamic child machines", () => { }).handle({ states: { Commissioning: { - invoke: (from) => - from.effect("commission-wave", ({ children, state }) => - Effect.forEach( - state.plants, - (input) => children.spawn(Plant(input.id), { input }), - { discard: true } - )).onDone((to) => - to.branch.Operating().resolve(({ target }) => target.decoded(new Operating({ reports: 0 }))) - ) - .onFailure((to) => to.none) + invoke: { + src: "source1", + id: "commission-wave", + input: (context) => context, + onDone: { target: targets2.root.Operating, decoded: () => (new Operating({ reports: 0 })) }, + onFailure: { none: true } + } }, Operating: { on: { - PlantReported: (to) => - to.branch.Operating().resolve(({ state, target }) => - target.decoded(new Operating({ reports: state.reports + 1 })) - ), - Grow: (to) => - to.branch.Commissioning().resolve(({ event, target }) => - target.decoded(new Commissioning({ plants: event.plants })) - ), - Decommission: (to) => - to.none.resolve(({ event }, enqueue) => { + PlantReported: { + target: targets2.root.Operating, + decoded: ({ state }) => (new Operating({ reports: state.reports + 1 })) + }, + Grow: { + target: targets2.root.Commissioning, + decoded: ({ event }) => (new Commissioning({ plants: event.plants })) + }, + Decommission: { + none: true, + resolve: ({ event }, enqueue) => { enqueue.stop(Plant(event.id)) - }) + } + } } } } @@ -224,19 +251,47 @@ describe("dynamic child machines", () => { "DuplicateRejected", {} ) {} + const root3 = Machine.state({ initial: "Starting", states: { Starting, DuplicateRejected } }) + const targets3 = Machine.targets(root3) const parentMachine = Machine.make({ - root: Machine.state({ initial: "Starting", states: { Starting, DuplicateRejected } }), + effects: { + source1: ({ + children + }: Machine.Machine.InvokeContext< + { + readonly "": { + readonly initial: "Starting" + readonly states: { + readonly Starting: typeof Starting + readonly DuplicateRejected: typeof DuplicateRejected + } + } & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [], + readonly [], + "Starting", + readonly [], + readonly [] + >) => + children.spawn(Child("same")).pipe( + Effect.andThen(children.spawn(Child("same"))) + ) + }, + + root: root3, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Starting.decoded(new Starting({})))) }).handle({ states: { Starting: { - invoke: (from) => - from.effect("spawn-duplicate", ({ children }) => - children.spawn(Child("same")).pipe( - Effect.andThen(children.spawn(Child("same"))) - )).onDone((to) => to.none).onFailure((to) => to.branch.DuplicateRejected()) + invoke: { + src: "source1", + id: "spawn-duplicate", + input: (context) => context, + onDone: { none: true }, + onFailure: { target: targets3.root.DuplicateRejected } + } }, DuplicateRejected: {} } @@ -285,15 +340,18 @@ describe("dynamic child machines", () => { }) const Supervisor = Machine.childAddress("supervisor") class Running extends Schema.TaggedClass("DynamicLogicRunning")("Running", {}) {} + const root4 = Machine.state({ initial: "Running", states: { Running } }) const parentMachine = Machine.make({ - root: Machine.state({ initial: "Running", states: { Running } }), + logic: { source1: supervisorLogic }, + + root: root4, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Running.decoded(new Running({})))) }).handle({ states: { Running: { - invoke: (from) => from.logic("supervisor", { address: Supervisor, logic: supervisorLogic }) + invoke: { src: "source1", id: "supervisor", address: Supervisor } } } }) @@ -321,8 +379,10 @@ describe("dynamic child machines", () => { count: Schema.Number }) {} class Increment extends Schema.TaggedClass("DynamicControlIncrement")("Increment", {}) {} + const root5 = Machine.state({ initial: "UnitActive", states: { UnitActive } }) + const targets5 = Machine.targets(root5) const unitMachine = Machine.make({ - root: Machine.state({ initial: "UnitActive", states: { UnitActive } }), + root: root5, events: Machine.eventsFromSchemas(Increment), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.UnitActive.decoded(new UnitActive({ count: 0 })))) @@ -330,10 +390,10 @@ describe("dynamic child machines", () => { states: { UnitActive: { on: { - Increment: (to) => - to.branch.UnitActive().resolve(({ state, target }) => - target.decoded(new UnitActive({ count: state.count + 1 })) - ) + Increment: { + target: targets5.root.UnitActive, + decoded: ({ state }) => (new UnitActive({ count: state.count + 1 })) + } } } } @@ -341,22 +401,47 @@ describe("dynamic child machines", () => { const Unit = Machine.childFamily(unitMachine) class Managing extends Schema.TaggedClass("DynamicControlManaging")("Managing", {}) {} class Ready extends Schema.TaggedClass("DynamicControlReady")("Ready", {}) {} + const root6 = Machine.state({ initial: "Managing", states: { Managing, Ready } }) + const targets6 = Machine.targets(root6) const parentMachine = Machine.make({ - root: Machine.state({ initial: "Managing", states: { Managing, Ready } }), + effects: { + source1: ({ + children + }: Machine.Machine.InvokeContext< + { + readonly "": { + readonly initial: "Managing" + readonly states: { readonly Managing: typeof Managing; readonly Ready: typeof Ready } + } & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [], + readonly [], + "Managing", + readonly [], + readonly [] + >) => + Effect.gen(function*() { + yield* children.spawn(Unit("kept")) + yield* children.spawn(Unit("stopped")) + yield* children.sendTo(Unit("kept"), new Increment({})) + yield* children.stop(Unit("stopped")) + }) + }, + + root: root6, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Managing.decoded(new Managing({})))) }).handle({ states: { Managing: { - invoke: (from) => - from.effect("control-units", ({ children }) => - Effect.gen(function*() { - yield* children.spawn(Unit("kept")) - yield* children.spawn(Unit("stopped")) - yield* children.sendTo(Unit("kept"), new Increment({})) - yield* children.stop(Unit("stopped")) - })).onDone((to) => to.branch.Ready()).onFailure((to) => to.none) + invoke: { + src: "source1", + id: "control-units", + input: (context) => context, + onDone: { target: targets6.root.Ready }, + onFailure: { none: true } + } }, Ready: {} } diff --git a/packages/effect-machine/test/machine/History.test.ts b/packages/effect-machine/test/machine/History.test.ts index 780e41a9..3d6de0ce 100644 --- a/packages/effect-machine/test/machine/History.test.ts +++ b/packages/effect-machine/test/machine/History.test.ts @@ -134,16 +134,26 @@ const makeCheckoutMachine = ( lifecycle?: Array, onDefault?: () => void, exactDefault?: () => ReturnType -) => - Machine.make({ +) => { + const targets1 = Machine.targets(CheckoutStates) + return Machine.make({ + branches: { + transition3: { destination: { history: targets1.root.checkout.exact } }, + transition4: { destination: { target: targets1.root.checkout.payment } }, + transition5: { destination: { history: targets1.root.checkout.recent } }, + transition6: { destination: { history: targets1.root.checkout.exact } } + }, + root: CheckoutStates, events: Machine.eventsFromSchemas(Leave, ResumeShallow, ResumeDeep, GoShipping, EnterVerifying, ReenterHistory), initialConfiguration: (root) => root.resolve(({ target }) => initial.state.path === "checkout" ? target.from((to) => - to.checkout.decoded(new Checkout({ orderId: "initial" }), (checkout) => - checkout.shipping.decoded(new Shipping({ address: "initial" }))) + to.checkout.decoded( + new Checkout({ orderId: "initial" }), + (checkout) => checkout.shipping.decoded(new Shipping({ address: "initial" })) + ) ) : initial ) @@ -171,24 +181,28 @@ const makeCheckoutMachine = ( } }, on: { - Leave: (to) => - to.branch.support().resolve(({ target }) => target.decoded(new Support({ ticket: "ticket-1" }))), - GoShipping: (to) => - to.local.shipping().resolve(({ event, target }) => - target.decoded(new Shipping({ address: event.address })) - ), - ReenterHistory: (to) => to.history.checkout.exact.reenter().resolve(({ target }) => target()) + Leave: { target: targets1.root.support, decoded: () => (new Support({ ticket: "ticket-1" })) }, + GoShipping: { + target: targets1.root.checkout.shipping, + decoded: ({ event }) => (new Shipping({ address: event.address })) + }, + ReenterHistory: { + branches: "transition3", + reenter: true, + resolve: ({ select: { destination: target } }) => target() + } }, states: { shipping: { on: { - EnterVerifying: (to) => - to.local.payment().resolve(({ target }) => + EnterVerifying: { + branches: "transition4", + resolve: ({ select: { destination: target } }) => target.decoded( new Payment({ attempt: 2 }), (payment) => payment.verifying.decoded(new Verifying({ challengeId: "challenge-7" })) ) - ) + } } }, payment: { @@ -223,12 +237,13 @@ const makeCheckoutMachine = ( lifecycle?.push("exit:support") }, on: { - ResumeShallow: (to) => to.history.checkout.recent.resolve(({ target }) => target()), - ResumeDeep: (to) => to.history.checkout.exact.resolve(({ target }) => target()) + ResumeShallow: { branches: "transition5", resolve: ({ select: { destination: target } }) => target() }, + ResumeDeep: { branches: "transition6", resolve: ({ select: { destination: target } }) => target() } } } } }) +} const waitForPath = ( actor: Machine.MachineRef, @@ -323,8 +338,14 @@ const activeWorkspace: Machine.Snapshot = { } } -const makeWorkspaceMachine = (initialized: Array) => - Machine.make({ +const makeWorkspaceMachine = (initialized: Array) => { + const targets2 = Machine.targets(WorkspaceStates) + return Machine.make({ + branches: { + transition2: { destination: { history: targets2.root.workspace.recent } }, + transition3: { destination: { history: targets2.root.workspace.exact } } + }, + root: WorkspaceStates, events: Machine.eventsFromSchemas(LeaveWorkspace, ResumeWorkspaceShallow, ResumeWorkspaceDeep), initialConfiguration: (root) => @@ -378,7 +399,7 @@ const makeWorkspaceMachine = (initialized: Array) => } }, on: { - LeaveWorkspace: (to) => to.branch.away().resolve(({ target }) => target.decoded(new Away({}))) + LeaveWorkspace: { target: targets2.root.away, decoded: () => (new Away({})) } }, states: { editor: { @@ -397,12 +418,16 @@ const makeWorkspaceMachine = (initialized: Array) => }, away: { on: { - ResumeWorkspaceShallow: (to) => to.history.workspace.recent.resolve(({ target }) => target()), - ResumeWorkspaceDeep: (to) => to.history.workspace.exact.resolve(({ target }) => target()) + ResumeWorkspaceShallow: { + branches: "transition2", + resolve: ({ select: { destination: target } }) => target() + }, + ResumeWorkspaceDeep: { branches: "transition3", resolve: ({ select: { destination: target } }) => target() } } } } }) +} const NestedHistoryStates = Machine.state({ initial: "workspace", @@ -452,7 +477,14 @@ const nestedParallelSnapshot: Machine.Snapshot = { } } +const targets3 = Machine.targets(NestedHistoryStates) const nestedHistoryMachine = Machine.make({ + branches: { + transition1: { destination: { history: targets3.root.workspace.editor.exact } }, + transition2: { destination: { history: targets3.root.workspace.editor.exact } }, + transition3: { destination: { history: targets3.root.workspace.editor.exact } } + }, + root: NestedHistoryStates, events: Machine.eventsFromSchemas(RestoreEditor, DefaultEditor), initialConfiguration: (root) => @@ -489,13 +521,17 @@ const nestedHistoryMachine = Machine.make({ states: { preview: { on: { - RestoreEditor: (to) => to.history.workspace.editor.exact.reenter().resolve(({ target }) => target()), - DefaultEditor: (to) => to.history.workspace.editor.exact.resolve(({ target }) => target()) + RestoreEditor: { + branches: "transition1", + reenter: true, + resolve: ({ select: { destination: target } }) => target() + }, + DefaultEditor: { branches: "transition2", resolve: ({ select: { destination: target } }) => target() } } }, writing: { on: { - DefaultEditor: (to) => to.history.workspace.editor.exact.resolve(({ target }) => target()) + DefaultEditor: { branches: "transition3", resolve: ({ select: { destination: target } }) => target() } } } } diff --git a/packages/effect-machine/test/machine/InitialEntry.test.ts b/packages/effect-machine/test/machine/InitialEntry.test.ts index fdbeb83d..5ebc8c9c 100644 --- a/packages/effect-machine/test/machine/InitialEntry.test.ts +++ b/packages/effect-machine/test/machine/InitialEntry.test.ts @@ -40,8 +40,9 @@ const States = Machine.state({ } }) -const makeMachine = () => - Machine.make({ +const makeMachine = () => { + const targets1 = Machine.targets(States) + return Machine.make({ root: States, events: Machine.eventsFromSchemas(Open, OpenInvalid), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.closed.decoded(new Closed({})))) @@ -49,8 +50,8 @@ const makeMachine = () => states: { closed: { on: { - Open: (to) => to.branch.opened.initial.resolve(({ target }) => target.from({ id: "team-1" })), - OpenInvalid: (to) => to.branch.opened.initial.resolve(({ target }) => target.from({ id: "" })) + Open: { initial: targets1.root.opened, from: () => ({ id: "team-1" }) }, + OpenInvalid: { initial: targets1.root.opened, from: () => ({ id: "" }) } } }, opened: { @@ -58,6 +59,7 @@ const makeMachine = () => } } }) +} const ParallelStates = Machine.state({ initial: "outside", @@ -78,8 +80,9 @@ const ParallelStates = Machine.state({ } }) -const makeParallelMachine = () => - Machine.make({ +const makeParallelMachine = () => { + const targets2 = Machine.targets(ParallelStates) + return Machine.make({ root: ParallelStates, events: Machine.eventsFromSchemas(EnterDashboard), initialConfiguration: (root) => @@ -88,7 +91,7 @@ const makeParallelMachine = () => states: { outside: { on: { - EnterDashboard: (to) => to.branch.dashboard.initial.resolve(({ target }) => target.decoded(new Dashboard({}))) + EnterDashboard: { initial: targets2.root.dashboard, decoded: () => (new Dashboard({})) } } }, dashboard: { @@ -101,6 +104,7 @@ const makeParallelMachine = () => } } }) +} const ChoiceStates = Machine.state({ initial: "outside", @@ -117,8 +121,9 @@ const ChoiceStates = Machine.state({ } }) -const makeChoiceMachine = () => - Machine.make({ +const makeChoiceMachine = () => { + const targets3 = Machine.targets(ChoiceStates) + return Machine.make({ root: ChoiceStates, events: Machine.eventsFromSchemas(EnterFlow), initialConfiguration: (root) => @@ -127,18 +132,19 @@ const makeChoiceMachine = () => states: { outside: { on: { - EnterFlow: (to) => to.branch.flow.initial.resolve(({ target }) => target.decoded(new Flow({}))) + EnterFlow: { initial: targets3.root.flow, decoded: () => (new Flow({})) } } }, flow: { states: { routing: { - choice: (to) => to.local.approved().resolve(({ target }) => target.decoded(new Approved({}))) + choice: { target: targets3.root.flow.approved, decoded: () => (new Approved({})) } } } } } }) +} const StructuralStates = Machine.state({ initial: "outside", @@ -151,8 +157,9 @@ const StructuralStates = Machine.state({ } }) -const makeStructuralMachine = () => - Machine.make({ +const makeStructuralMachine = () => { + const targets4 = Machine.targets(StructuralStates) + return Machine.make({ root: StructuralStates, events: Machine.eventsFromSchemas(EnterFlow), initialConfiguration: (root) => @@ -161,11 +168,12 @@ const makeStructuralMachine = () => states: { outside: { on: { - EnterFlow: (to) => to.branch.group.initial.resolve(({ target }) => target.from()) + EnterFlow: { initial: targets4.root.group } } } } }) +} const NestedStates = Machine.state({ initial: "root", @@ -184,8 +192,9 @@ const NestedStates = Machine.state({ } }) -const makeNestedMachine = () => - Machine.make({ +const makeNestedMachine = () => { + const targets5 = Machine.targets(NestedStates) + return Machine.make({ root: NestedStates, events: Machine.eventsFromSchemas(OpenLocal, OpenBranch), initialConfiguration: (root) => @@ -196,8 +205,8 @@ const makeNestedMachine = () => states: { closed: { on: { - OpenLocal: (to) => to.local.opened.initial.resolve(({ target }) => target.from({ id: "local" })), - OpenBranch: (to) => to.branch.root.opened.initial.resolve(({ target }) => target.from({ id: "branch" })) + OpenLocal: { initial: targets5.root.root.opened, from: () => ({ id: "local" }) }, + OpenBranch: { initial: targets5.root.root.opened, from: () => ({ id: "branch" }) } } }, opened: { @@ -207,6 +216,7 @@ const makeNestedMachine = () => } } }) +} describe("declared initial entry", () => { it.effect("captures the target-first selector once and evaluates its resolver only when planned", () => diff --git a/packages/effect-machine/test/machine/Inspection.test.ts b/packages/effect-machine/test/machine/Inspection.test.ts index ca635a3f..d9cc1fa1 100644 --- a/packages/effect-machine/test/machine/Inspection.test.ts +++ b/packages/effect-machine/test/machine/Inspection.test.ts @@ -65,6 +65,7 @@ const ChoiceStates = Machine.state({ } }) +const targets1 = Machine.targets(ChoiceStates) const choiceMachine = Machine.make({ root: ChoiceStates, events: Machine.eventsFromSchemas(), @@ -75,7 +76,7 @@ const choiceMachine = Machine.make({ Flow: { states: { Routing: { - choice: (to) => to.local.Ready().resolve(({ target }) => target.decoded(new Ready({}))) + choice: { target: targets1.root.Flow.Ready, decoded: () => (new Ready({})) } } } } diff --git a/packages/effect-machine/test/machine/Invoke.test.ts b/packages/effect-machine/test/machine/Invoke.test.ts index d4bbade1..8ff498f1 100644 --- a/packages/effect-machine/test/machine/Invoke.test.ts +++ b/packages/effect-machine/test/machine/Invoke.test.ts @@ -24,47 +24,36 @@ class FinishStream extends Schema.TaggedClass("InvokeFinishStream" const States = Machine.state({ initial: "Idle", states: { Idle, Loading, Complete, Failed } }) describe("inline invoke", () => { - it("exposes only source-relevant, unhandled lifecycle methods", () => { - let inspected = false - Machine.make({ + it("captures a timer with its completion channel", () => { + const machine = Machine.make({ root: States, events: Machine.eventsFromSchemas(), + timers: { timeout: "1 second" }, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) - }).handle({ - states: { - Loading: { - invoke: (from) => { - const timer = from.timer("timeout", "1 second") - assert.isFalse("onFailure" in timer) - assert.isFalse("onElement" in timer) - assert.isFalse("onSnapshot" in timer) - const completed = timer.onDone((to) => to.none) - assert.isFalse("onDone" in completed) - inspected = true - return completed - } - }, - Complete: {}, - Failed: {}, - Idle: {} - } - }) - assert.isTrue(inspected) + }).handle({ states: { Loading: { invoke: { src: "timeout", onDone: { none: true } } } } }) + const definitions = Machine.transitionDefinitions(machine) + assert.lengthOf(definitions, 1) + assert.deepStrictEqual(definitions[0]?.trigger, { type: "invoke", id: "timeout", outcome: "done" }) }) it.effect("ignores an invocation outcome when its transition declines", () => Effect.gen(function*() { + const targets2 = Machine.targets(States) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets2.root.Complete } } }, + effects: { source1: Effect.suspend(() => Effect.succeed("ignored")) }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Effect.succeed("ignored")).onDone((to) => - to.branch.Complete().resolve(({ decline }) => decline(), { declinable: true }) - ) + invoke: { + src: "source1", + id: "load", + onDone: { branches: "transition1", resolve: ({ decline }) => decline(), declinable: true } + } }, Complete: {}, Failed: {}, @@ -81,7 +70,10 @@ describe("inline invoke", () => { it.effect("handles Stream elements sequentially before completion", () => Effect.gen(function*() { const states = Machine.state({ initial: "Collecting", states: { Collecting, Complete } }) + const targets3 = Machine.targets(states) const definition = Machine.make({ + streams: { source1: Stream.suspend(() => Stream.fromIterable([1, 2, 3])) }, + root: states, events: Machine.eventsFromSchemas(Add), initialConfiguration: (root) => @@ -90,21 +82,25 @@ describe("inline invoke", () => { const machine = definition.handle({ states: { Collecting: { - invoke: (from) => - from.stream("numbers", () => Stream.fromIterable([1, 2, 3])).onElement((to) => - to.none.resolve(({ element }, enqueue) => { + invoke: { + src: "source1", + id: "numbers", + onElement: { + none: true, + resolve: ({ element }, enqueue) => { enqueue.raise(new Add({ value: element })) - }) - ).onDone((to) => - to.branch.Complete().resolve(({ state, target }) => - target.decoded(new Complete({ value: state.values.join(",") })) - ) - ), + } + }, + onDone: { + target: targets3.root.Complete, + decoded: ({ state }) => (new Complete({ value: state.values.join(",") })) + } + }, on: { - Add: (to) => - to.branch.Collecting().resolve(({ event, state, target }) => - target.decoded(new Collecting({ values: [...state.values, event.value] })) - ) + Add: { + target: targets3.root.Collecting, + decoded: ({ event, state }) => (new Collecting({ values: [...state.values, event.value] })) + } } }, Complete: {} @@ -164,7 +160,10 @@ describe("inline invoke", () => { it.effect("routes a Stream typed failure through onFailure", () => Effect.gen(function*() { + const targets4 = Machine.targets(States) const definition = Machine.make({ + streams: { source1: Stream.suspend(() => Stream.fail("offline")) }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) @@ -172,10 +171,12 @@ describe("inline invoke", () => { const machine = definition.handle({ states: { Loading: { - invoke: (from) => - from.stream("updates", () => Stream.fail("offline")).onDone((to) => to.none).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => target.decoded(new Failed({ message: error }))) - ) + invoke: { + src: "source1", + id: "updates", + onDone: { none: true }, + onFailure: { target: targets4.root.Failed, decoded: ({ error }) => (new Failed({ message: error })) } + } }, Complete: {}, Failed: {} @@ -198,6 +199,8 @@ describe("inline invoke", () => { Effect.gen(function*() { const defect = new Error("stream defect") const definition = Machine.make({ + streams: { source1: Stream.suspend(() => Stream.die(defect)) }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) @@ -205,7 +208,7 @@ describe("inline invoke", () => { const machine = definition.handle({ states: { Loading: { - invoke: (from) => from.stream("updates", () => Stream.die(defect)).onDone((to) => to.none) + invoke: { src: "source1", id: "updates", onDone: { none: true } } }, Complete: {}, Failed: {} @@ -230,7 +233,10 @@ describe("inline invoke", () => { finalized = true })) ) + const targets6 = Machine.targets(States) const definition = Machine.make({ + streams: { source1: Stream.suspend(() => source) }, + root: States, events: Machine.eventsFromSchemas(FinishStream), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) @@ -238,17 +244,22 @@ describe("inline invoke", () => { const machine = definition.handle({ states: { Loading: { - invoke: (from) => - from.stream("updates", () => source).onElement((to) => - to.none.resolve(({ element }, enqueue) => { + invoke: { + src: "source1", + id: "updates", + onElement: { + none: true, + resolve: ({ element }, enqueue) => { enqueue.raise(new FinishStream({ value: element })) - }) - ).onDone((to) => to.none), + } + }, + onDone: { none: true } + }, on: { - FinishStream: (to) => - to.branch.Complete().resolve(({ event, target }) => - target.decoded(new Complete({ value: String(event.value) })) - ) + FinishStream: { + target: targets6.root.Complete, + decoded: ({ event }) => (new Complete({ value: String(event.value) })) + } } }, Complete: {}, @@ -270,17 +281,21 @@ describe("inline invoke", () => { it.effect("plans a successful Effect outcome directly", () => Effect.gen(function*() { + const targets7 = Machine.targets(States) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.succeed("ready")) }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Effect.succeed("ready")).onDone((to) => - to.branch.Complete().resolve(({ output, target }) => target.decoded(new Complete({ value: output }))) - ) + invoke: { + src: "source1", + id: "load", + onDone: { target: targets7.root.Complete, decoded: ({ output }) => (new Complete({ value: output })) } + } }, Complete: {}, Failed: {} @@ -310,17 +325,21 @@ describe("inline invoke", () => { it.effect("plans a typed Effect failure directly", () => Effect.gen(function*() { + const targets8 = Machine.targets(States) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.fail("offline")) }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.from())) }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Effect.fail("offline")).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => target.decoded(new Failed({ message: error }))) - ) + invoke: { + src: "source1", + id: "load", + onFailure: { target: targets8.root.Failed, decoded: ({ error }) => (new Failed({ message: error })) } + } }, Complete: {}, Failed: {} @@ -338,7 +357,14 @@ describe("inline invoke", () => { it.effect("fails the owning machine when an Effect source factory defects", () => Effect.gen(function*() { const defect = new Error("source defect") + const targets9 = Machine.targets(States) const machine = Machine.make({ + effects: { + source1: Effect.suspend((): Effect.Effect => { + throw defect + }) + }, + root: States, events: Machine.eventsFromSchemas(Start), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.from())) @@ -346,14 +372,11 @@ describe("inline invoke", () => { states: { Idle: { on: { - Start: (to) => to.branch.Loading().resolve(({ target }) => target.from()) + Start: { target: targets9.root.Loading } } }, Loading: { - invoke: (from) => - from.effect("load", (): Effect.Effect => { - throw defect - }).onDone((to) => to.none) + invoke: { src: "source1", id: "load", onDone: { none: true } } }, Complete: {}, Failed: {} @@ -377,7 +400,10 @@ describe("inline invoke", () => { initial: () => Effect.fail(failure), run: () => Effect.never }) + const targets10 = Machine.targets(States) const machine = Machine.make({ + logic: { source1: logic }, + root: States, events: Machine.eventsFromSchemas(Start), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.from())) @@ -385,11 +411,11 @@ describe("inline invoke", () => { states: { Idle: { on: { - Start: (to) => to.branch.Loading().resolve(({ target }) => target.from()) + Start: { target: targets10.root.Loading } } }, Loading: { - invoke: (from) => from.logic("worker", { address: Machine.childAddress("worker"), logic: logic }) + invoke: { src: "source1", id: "worker", address: Machine.childAddress("worker") } }, Complete: {}, Failed: {} diff --git a/packages/effect-machine/test/machine/LiveInspection.test.ts b/packages/effect-machine/test/machine/LiveInspection.test.ts index d2ae0f90..c68b374d 100644 --- a/packages/effect-machine/test/machine/LiveInspection.test.ts +++ b/packages/effect-machine/test/machine/LiveInspection.test.ts @@ -14,7 +14,10 @@ const states = Machine.state({ initial: "Idle", states: { Idle } }) const Events = Machine.eventsFromSchemas(Increment) const Emissions = Machine.emittedEventsFromSchemas(Notice) +const targets1 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets1.root.Idle } } }, + id: "counter", root: states, events: Events, @@ -24,11 +27,13 @@ const machine = Machine.make({ states: { Idle: { on: { - Increment: (to) => - to.branch.Idle().resolve(({ event, target }, enqueue) => { + Increment: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }, enqueue) => { enqueue.emit(Emissions.Notice({ value: event.by })) return target.decoded(new Idle({})) - }) + } + } } } } @@ -85,7 +90,7 @@ describe("Machine live inspection", () => { trigger: { type: "event", event: "Increment" }, reenter: false, branchIndex: 0, - branchKey: undefined, + branchKey: "destination", target: "Idle", resolvedTarget: "Idle", updates: [] @@ -128,6 +133,8 @@ describe("Machine live inspection", () => { it.effect("represents Effect invokes as owned activities rather than child machines", () => Effect.scoped(Effect.gen(function*() { const active = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.never) }, + id: "activity-root", root: states, events: Machine.eventsFromSchemas(), @@ -135,7 +142,7 @@ describe("Machine live inspection", () => { }).handle({ states: { Idle: { - invoke: (from) => from.effect("worker", () => Effect.never) + invoke: { src: "source1", id: "worker" } } } }) @@ -179,6 +186,8 @@ describe("Machine live inspection", () => { it.effect("represents Stream invokes as owned Stream activities", () => Effect.scoped(Effect.gen(function*() { const active = Machine.make({ + streams: { source1: Stream.suspend(() => Stream.never) }, + id: "stream-activity-root", root: states, events: Machine.eventsFromSchemas(), @@ -186,7 +195,7 @@ describe("Machine live inspection", () => { }).handle({ states: { Idle: { - invoke: (from) => from.stream("updates", () => Stream.never).onDone((to) => to.none) + invoke: { src: "source1", id: "updates", onDone: { none: true } } } } }) @@ -232,11 +241,13 @@ describe("Machine live inspection", () => { states: { ChildIdle: { on: { - Trigger: (to) => - to.none.resolve(({ parent }, enqueue) => { + Trigger: { + none: true, + resolve: ({ parent }, enqueue) => { enqueue.sendTo(parent, ParentEvents.ChildReady()) return undefined - }) + } + } } } } @@ -249,7 +260,10 @@ describe("Machine live inspection", () => { ParentDone: { schema: ParentDone, type: "final" } } }) + const targets5 = Machine.targets(parentStates) const parentMachine = Machine.make({ + children: { source1: Child }, + id: "parent-machine", root: parentStates, events: Machine.eventsFromSchemas(ParentEvents), @@ -258,9 +272,9 @@ describe("Machine live inspection", () => { }).handle({ states: { ParentIdle: { - invoke: (from) => from.child(Child), + invoke: { src: "source1" }, on: { - ChildReady: (to) => to.branch.ParentDone().resolve(({ target }) => target.decoded(new ParentDone({}))) + ChildReady: { target: targets5.root.ParentDone, decoded: () => (new ParentDone({})) } } }, ParentDone: {} diff --git a/packages/effect-machine/test/machine/LocalTargetWith.test.ts b/packages/effect-machine/test/machine/LocalTargetWith.test.ts index b79568c2..ebc7210a 100644 --- a/packages/effect-machine/test/machine/LocalTargetWith.test.ts +++ b/packages/effect-machine/test/machine/LocalTargetWith.test.ts @@ -27,7 +27,10 @@ describe("local compound target selection", () => { } } }) + const targets1 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets1.root.search } } }, + root: states, events: Machine.eventsFromSchemas(Events), initialConfiguration: (root) => @@ -38,16 +41,18 @@ describe("local compound target selection", () => { states: { search: { on: { - UpdateQuery: (to) => - to.local.with.reenter().resolve( - ({ event, target }) => target.from({ query: event.query }, (search) => search.Updated.from()) - ) + UpdateQuery: { + branches: "transition1", + reenter: true, + resolve: ({ event, select: { destination: target } }) => + target.from({ query: event.query }, (search) => search.Updated.from()) + } }, states: { Idle: {}, Updated: { on: { - Reset: (to) => to.local.Idle().resolve(({ target }) => target.from()) + Reset: { target: targets1.root.search.Idle } } } } @@ -61,9 +66,11 @@ describe("local compound target selection", () => { reenter: true, acceptance: "required", branches: [{ - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "search", - selection: { path: "search", kind: "state", scope: "local" }, + selection: { path: "search", kind: "state", scope: "branch" }, updates: [] }] }, { @@ -74,7 +81,7 @@ describe("local compound target selection", () => { branches: [{ type: "direct", target: "search.Idle", - selection: { path: "search.Idle", kind: "state", scope: "local" }, + selection: { path: "search.Idle", kind: "state", scope: "branch" }, updates: [] }] }]) @@ -122,7 +129,11 @@ describe("local compound target selection", () => { } } }) + const targets2 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets2.root.search } } }, + effects: { source1: Effect.suspend(() => Effect.succeed("resolved")) }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -134,12 +145,15 @@ describe("local compound target selection", () => { search: { states: { Searching: { - invoke: (from) => - from.effect("search", () => Effect.succeed("resolved")).onDone((to) => - to.local.with.resolve(({ output, target }) => + invoke: { + src: "source1", + id: "search", + onDone: { + branches: "transition1", + resolve: ({ output, select: { destination: target } }) => target.from({ query: output }, (search) => search.Updated.from()) - ) - ) + } + } }, Updated: {} } @@ -153,9 +167,11 @@ describe("local compound target selection", () => { reenter: false, acceptance: "required", branches: [{ - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "search", - selection: { path: "search", kind: "state", scope: "local" }, + selection: { path: "search", kind: "state", scope: "branch" }, updates: [] }] }]) @@ -188,6 +204,8 @@ describe("local compound target selection", () => { } }) + const targets3 = Machine.targets(states) + assert.notProperty(targets3.root.flow, "with") Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), @@ -199,10 +217,7 @@ describe("local compound target selection", () => { states: { Idle: { on: { - Advance: (to) => { - assert.notProperty(to.local, "with") - return to.local.Updated().resolve(({ target }) => target.from()) - } + Advance: { target: targets3.root.flow.Updated } } }, Updated: {} diff --git a/packages/effect-machine/test/machine/Machine.test.ts b/packages/effect-machine/test/machine/Machine.test.ts index 68511d83..8c3683cb 100644 --- a/packages/effect-machine/test/machine/Machine.test.ts +++ b/packages/effect-machine/test/machine/Machine.test.ts @@ -129,7 +129,7 @@ describe("Machine", () => { initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Stable.decoded(new Stable({})))) }) - const handlingPing = definition.handle({ states: { Stable: { on: { Ping: (to) => to.none } } } }) + const handlingPing = definition.handle({ states: { Stable: { on: { Ping: { none: true } } } } }) const ignoringPing = definition.handle({ states: { Stable: {} } }) assert.isFalse("handle" in handlingPing) @@ -148,6 +148,16 @@ describe("Machine", () => { const states = Machine.state({ initial: "Stable", states: { Stable } }) let captures = 0 let resolves = 0 + const transition = { + none: true as const, + get resolve() { + captures++ + return (): undefined => { + resolves++ + return undefined + } + } + } const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Ping), @@ -157,12 +167,7 @@ describe("Machine", () => { states: { Stable: { on: { - Ping: (to) => { - captures++ - return to.none.resolve(() => { - resolves++ - }) - } + Ping: transition } } } @@ -199,13 +204,15 @@ describe("Machine", () => { class Idle extends Schema.TaggedClass("BareTargetIdle")("Idle", {}) {} class Done extends Schema.TaggedClass("BareTargetDone")("Done", {}) {} class Finish extends Schema.TaggedClass("BareTargetFinish")("Finish", {}) {} + const root3 = Machine.state({ initial: "Idle", states: { Idle, Done } }) + const targets3 = Machine.targets(root3) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Done } }), + root: root3, events: Machine.eventsFromSchemas(Finish), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.from())) }).handle({ states: { - Idle: { on: { Finish: (to) => to.branch.Done() } }, + Idle: { on: { Finish: { target: targets3.root.Done } } }, Done: {} } }) @@ -222,13 +229,14 @@ describe("Machine", () => { Effect.gen(function*() { class Stable extends Schema.TaggedClass("ResolverFreeReentryStable")("Stable", {}) {} class Restart extends Schema.TaggedClass("ResolverFreeReentryRestart")("Restart", {}) {} + const root4 = Machine.state({ initial: "Stable", states: { Stable } }) const machine = Machine.make({ - root: Machine.state({ initial: "Stable", states: { Stable } }), + root: root4, events: Machine.eventsFromSchemas(Restart), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Stable.from())) }).handle({ states: { - Stable: { on: { Restart: (to) => to.none.reenter() } } + Stable: { on: { Restart: { none: true, reenter: true } } } } }) @@ -246,9 +254,13 @@ describe("Machine", () => { class Stable extends Schema.TaggedClass("NamedBranchStable")("Stable", {}) {} class Ping extends Schema.TaggedClass("NamedBranchPing")("Ping", { route: Schema.Boolean }) {} const states = Machine.state({ initial: "Stable", states: { Stable } }) - let captures = 0 - let declarations: any + const targets5 = Machine.targets(states) + const declarations = { + unchanged: { none: true as const }, + refresh: { title: "Refresh stable state", target: targets5.root.Stable } + } const definition = Machine.make({ + branches: { refresh: declarations }, root: states, events: Machine.eventsFromSchemas(Ping), initialConfiguration: (root) => @@ -258,18 +270,11 @@ describe("Machine", () => { states: { Stable: { on: { - Ping: (to) => { - captures++ - const captured = { - unchanged: { target: to.none }, - refresh: { title: "Refresh stable state", target: to.branch.Stable() } - } - declarations = captured - return to.branches(captured).reenter().resolve(({ event, select }) => - event.route - ? select.refresh.decoded(new Stable({})) - : select.unchanged() - ) + Ping: { + branches: "refresh", + reenter: true, + resolve: ({ event, select }) => + event.route ? select.refresh.decoded(new Stable({})) : select.unchanged() } } } @@ -278,7 +283,6 @@ describe("Machine", () => { declarations.refresh.title = "mutated after capture" - assert.strictEqual(captures, 1) assert.deepStrictEqual(Machine.transitionDefinitions(machine), [{ source: "Stable", trigger: { type: "event", event: "Ping" }, @@ -308,40 +312,26 @@ describe("Machine", () => { assert.strictEqual(refresh.microsteps[0]?.transitions[0]?.branchKey, "refresh") assert.deepStrictEqual(unchanged.microsteps[0]?.exitPaths, ["Stable"]) assert.deepStrictEqual(unchanged.microsteps[0]?.entryPaths, ["Stable"]) - assert.strictEqual(captures, 1) })) it("rejects branch records without stable string identities", () => { class Stable extends Schema.TaggedClass("InvalidBranchStable")("Stable", {}) {} class Ping extends Schema.TaggedClass("InvalidBranchPing")("Ping", {}) {} - const makeDefinition = () => + const handle = (group: object) => () => Machine.make({ root: Machine.state({ initial: "Stable", states: { Stable } }), events: Machine.eventsFromSchemas(Ping), + branches: { invalid: group } as any, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Stable.decoded(new Stable({})))) - }) - const handle = (branches: (to: any) => object) => () => - makeDefinition().handle({ - states: { - Stable: { - on: { - Ping: ((to: any) => to.branches(branches(to)).resolve((() => undefined) as any)) as any - } - } - } - }) - - assert.throws(handle(() => ({})), /requires a branch/) - assert.throws(handle((to) => [{ target: to.none }]), /requires a branch record/) - assert.throws(handle((to) => ({ "": { target: to.none } })), /non-index string branch keys/) - assert.throws(handle((to) => ({ 0: { target: to.none } })), /non-index string branch keys/) - assert.throws(handle((to) => ({ invalid: { title: "", target: to.none } })), /non-empty string/) - assert.throws(handle(() => ({ invalid: { target: undefined } })), /must select exactly one target/) - assert.throws( - handle((to) => ({ valid: { target: to.none }, [Symbol("invalid")]: { target: to.none } })), - /cannot use symbol keys/ - ) + }).handle({ states: { Stable: { on: { Ping: { branches: "invalid", resolve: () => undefined } as any } } } }) + assert.throws(handle({}), /requires a branch/) + assert.throws(handle([{ none: true }]), /branch record/) + assert.throws(handle({ "": { none: true } }), /non-index string branch keys/) + assert.throws(handle({ 0: { none: true } }), /non-index string branch keys/) + assert.throws(handle({ invalid: { title: "", none: true } }), /non-empty string/) + assert.throws(handle({ invalid: { target: undefined } }), /exactly one destination operation/) + assert.throws(handle({ valid: { none: true }, [Symbol("invalid")]: { none: true } }), /cannot use symbol keys/) }) it.effect("rejects selected branch evidence from another transition", () => @@ -350,8 +340,11 @@ describe("Machine", () => { class Capture extends Schema.TaggedClass("OwnedBranchCapture")("Capture", {}) {} class Reuse extends Schema.TaggedClass("OwnedBranchReuse")("Reuse", {}) {} let captured: unknown + const root6 = Machine.state({ initial: "Stable", states: { Stable } }) const machine = Machine.make({ - root: Machine.state({ initial: "Stable", states: { Stable } }), + branches: { transition1: { unchanged: { none: true } }, transition2: { unchanged: { none: true } } }, + + root: root6, events: Machine.eventsFromSchemas(Capture, Reuse), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Stable.decoded(new Stable({})))) @@ -359,12 +352,17 @@ describe("Machine", () => { states: { Stable: { on: { - Capture: (to) => - to.branches({ unchanged: { target: to.none } }).resolve(({ select }) => { + Capture: { + branches: "transition1", + resolve: ({ select }) => { captured = select.unchanged() - return captured as any - }), - Reuse: (to) => to.branches({ unchanged: { target: to.none } }).resolve(() => captured as any) + return captured as ReturnType + } + }, + Reuse: { + branches: "transition2", + resolve: ({ select }) => captured as ReturnType + } } } } @@ -570,7 +568,10 @@ describe("Machine", () => { Effect.gen(function*() { class Convert extends Schema.TaggedClass("Convert")("Convert", {}) {} const states = Machine.state({ initial: "Submit", states: { Submit, RequestSucceeded } }) + const targets7 = Machine.targets(states) const definition = Machine.make({ + branches: { transition1: { destination: { target: targets7.root.RequestSucceeded } } }, + root: states, events: Machine.eventsFromSchemas(Convert), initialConfiguration: (root) => @@ -580,11 +581,13 @@ describe("Machine", () => { states: { Submit: { on: { - Convert: (to) => - to.branch.RequestSucceeded().resolve(({ state, target }) => { + Convert: { + branches: "transition1", + resolve: ({ state, select: { destination: target } }) => { const { _tag: _, ...fields } = state return target.from(fields) - }) + } + } } } } @@ -605,6 +608,7 @@ describe("Machine", () => { it("make stores the machine id", () => { const states = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets8 = Machine.targets(states) const machine = Machine.make({ id: "UserMachine", root: states, @@ -618,10 +622,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => { - return target.decoded(new Loading({ requestId: "request-1" })) - }) + Submit: { target: targets8.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -891,6 +892,7 @@ describe("Machine", () => { const states = Machine.state({ initial: "Active", states: { Active: State } }) const events = Machine.eventsFromSchemas(PublicEvent, Defaulted, FiniteEvent) const internalEvents = Machine.internalEventsFromSchemas(InternalEvent) + const targets9 = Machine.targets(states) const definition = Machine.make({ root: states, events, @@ -902,21 +904,22 @@ describe("Machine", () => { states: { Active: { on: { - SetValue: (to) => - to.branch.Active().resolve(({ event, target }) => target.from({ value: event.value })), - Reset: (to) => - to.none.resolve((_, enqueue) => { + SetValue: { target: targets9.root.Active, from: ({ event }) => ({ value: event.value }) }, + Reset: { + none: true, + resolve: (_, enqueue) => { enqueue.raise(internalEvents.Loaded({ value: "loaded" })) return undefined - }), - Defaulted: (to) => - to.branch.Active().resolve(({ event, target }) => - target.from({ value: event.label ?? "default-label" }) - ), - Loaded: (to) => to.branch.Active().resolve(({ event, target }) => target.from({ value: event.value })), - TimedOut: (to) => to.branch.Active().resolve(({ target }) => target.from({ value: "timed-out" })), - Alpha: (to) => to.branch.Active().resolve(({ event, target }) => target.from({ value: event.value })), - Beta: (to) => to.branch.Active().resolve(({ event, target }) => target.from({ value: event.value })) + } + }, + Defaulted: { + target: targets9.root.Active, + from: ({ event }) => ({ value: event.label ?? "default-label" }) + }, + Loaded: { target: targets9.root.Active, from: ({ event }) => ({ value: event.value }) }, + TimedOut: { target: targets9.root.Active, from: () => ({ value: "timed-out" }) }, + Alpha: { target: targets9.root.Active, from: ({ event }) => ({ value: event.value }) }, + Beta: { target: targets9.root.Active, from: ({ event }) => ({ value: event.value }) } } } } @@ -980,7 +983,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => to.none + Submit: { none: true } } } } @@ -1022,7 +1025,11 @@ describe("Machine", () => { const release = yield* Deferred.make() const InternalEvent = Schema.TaggedUnion({ Loaded: {}, TimedOut: {} }) const states = Machine.state({ initial: "Loading", states: { Loading: {}, Waiting: {}, Done: {} } }) + const targets11 = Machine.targets(states) const definition = Machine.make({ + effects: { source1: Effect.suspend(() => Deferred.await(release)) }, + timers: { source2: "1 second" }, + root: states, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(InternalEvent), @@ -1031,16 +1038,10 @@ describe("Machine", () => { const machine = definition.handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Deferred.await(release)).onDone((to) => - to.branch.Waiting().resolve(({ target }) => target.from()) - ) + invoke: { src: "source1", id: "load", onDone: { target: targets11.root.Waiting } } }, Waiting: { - invoke: (from) => - from.timer("timeout", "1 second").onDone((to) => - to.branch.Done().resolve(({ target }) => target.from()) - ) + invoke: { src: "source2", id: "timeout", onDone: { target: targets11.root.Done } } }, Done: {} } @@ -1081,7 +1082,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => to.none + Submit: { none: true } } } } @@ -1134,6 +1135,7 @@ describe("Machine", () => { } } }) + const targets13 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), @@ -1142,8 +1144,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Done().resolve(({ event, target }) => target.from({ requestId: event.requestId })) + Submit: { target: targets13.root.Done, from: ({ event }) => ({ requestId: event.requestId }) } } }, Done: {} @@ -1225,7 +1226,13 @@ describe("Machine", () => { } } }) + const targets14 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition3: { destination: { target: targets14.root.Flow.Nested } }, + transition4: { destination: { target: targets14.root.Flow } } + }, + id: "from-empty-targets", root: states, events: Machine.eventsFromSchemas( @@ -1243,17 +1250,19 @@ describe("Machine", () => { states: { Idle: { on: { - Local: (to) => to.local.Running().resolve(({ target }) => target.from()), - LocalWith: (to) => to.local.Running().resolve(({ target }) => target.from()), - Branch: (to) => - to.branch.Flow.Nested().resolve(({ target }) => + Local: { target: targets14.root.Flow.Running }, + LocalWith: { target: targets14.root.Flow.Running }, + Branch: { + branches: "transition3", + resolve: ({ select: { destination: target } }) => target.from((nested) => nested.NestedIdle.from()) - ), - Full: (to) => - to.branch.Flow().resolve(({ target }) => + }, + Full: { + branches: "transition4", + resolve: ({ select: { destination: target } }) => target.from((flow) => flow.Nested.from((nested) => nested.NestedIdle.from())) - ), - Finish: (to) => to.local.Done().resolve(({ target }) => target.from()) + }, + Finish: { target: targets14.root.Flow.Done } } }, Running: {}, @@ -1385,6 +1394,7 @@ describe("Machine", () => { it.effect("fails invalid transition construction in the typed machine error channel", () => Effect.gen(function*() { const states = Machine.state({ initial: "NonEmptyIdle", states: { NonEmptyIdle, NonEmptyLoading } }) + const targets15 = Machine.targets(states) const machine = Machine.make({ id: "from-transition-refinement", root: states, @@ -1395,8 +1405,7 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => - to.branch.NonEmptyLoading().resolve(({ target }) => target.from({ requestId: "" })) + NonEmptySubmit: { target: targets15.root.NonEmptyLoading, from: () => ({ requestId: "" }) } } } } @@ -1445,7 +1454,10 @@ describe("Machine", () => { } } }) + const targets16 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets16.root.fulfillment } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => @@ -1454,8 +1466,9 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.fulfillment().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.from( { id: event.value }, (fulfillment) => @@ -1469,7 +1482,7 @@ describe("Machine", () => { (shipping) => shipping.quoted.from({ quoteId: event.value }) ) ) - ) + } } } } @@ -1513,7 +1526,10 @@ describe("Machine", () => { } } }) + const targets17 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets17.root.payment } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => @@ -1528,13 +1544,14 @@ describe("Machine", () => { states: { entering: { on: { - Submit: (to) => - to.branch.payment().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.from( { id: "payment-2" }, (payment) => payment.authorized.from({ code: event.value }) ) - ) + } } } } @@ -1572,7 +1589,10 @@ describe("Machine", () => { } } }) + const targets18 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets18.root.workflow } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => @@ -1587,8 +1607,9 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.workflow().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.from( { id: "workflow-2" }, (workflow) => @@ -1597,7 +1618,7 @@ describe("Machine", () => { (checkout) => checkout.quoted.from({ quoteId: event.value }) ) ) - ) + } } } } @@ -1658,6 +1679,7 @@ describe("Machine", () => { it.effect("decodes incoming events before handler selection", () => Effect.gen(function*() { const states = Machine.state({ initial: "NonEmptyIdle", states: { NonEmptyIdle } }) + const targets19 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(NonEmptySubmit), @@ -1669,7 +1691,7 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => to.branch.NonEmptyIdle().resolve(({ state, target }) => target.decoded(state)) + NonEmptySubmit: { target: targets19.root.NonEmptyIdle, decoded: ({ state }) => state } } } } @@ -1707,6 +1729,7 @@ describe("Machine", () => { it.effect("surfaces sent event decode failures through the machine lifecycle", () => Effect.gen(function*() { const states = Machine.state({ initial: "NonEmptyIdle", states: { NonEmptyIdle } }) + const targets20 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(NonEmptySubmit), @@ -1718,7 +1741,7 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => to.branch.NonEmptyIdle().resolve(({ state, target }) => target.decoded(state)) + NonEmptySubmit: { target: targets20.root.NonEmptyIdle, decoded: ({ state }) => state } } } } @@ -1747,6 +1770,7 @@ describe("Machine", () => { it.effect("decodes transition target values before accepting them", () => Effect.gen(function*() { const states = Machine.state({ initial: "NonEmptyIdle", states: { NonEmptyIdle, NonEmptyLoading } }) + const targets21 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(NonEmptySubmit), @@ -1758,10 +1782,10 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => - to.branch.NonEmptyLoading().resolve(({ target }) => - target.decoded(unsafeTagged({ _tag: "NonEmptyLoading", requestId: "" })) - ) + NonEmptySubmit: { + target: targets21.root.NonEmptyLoading, + decoded: () => (unsafeTagged({ _tag: "NonEmptyLoading", requestId: "" })) + } } } } @@ -1785,6 +1809,7 @@ describe("Machine", () => { it.effect("decodes same-state atomic snapshot targets in the compiled runtime", () => Effect.gen(function*() { const states = Machine.state({ initial: "NonEmptyIdle", states: { NonEmptyIdle } }) + const targets22 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(NonEmptySubmit), @@ -1796,10 +1821,10 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => - to.branch.NonEmptyIdle().resolve(({ target }) => - target.decoded(unsafeTagged({ _tag: "NonEmptyIdle", userId: "" })) - ) + NonEmptySubmit: { + target: targets22.root.NonEmptyIdle, + decoded: () => (unsafeTagged({ _tag: "NonEmptyIdle", userId: "" })) + } } } } @@ -1830,6 +1855,7 @@ describe("Machine", () => { } } }) + const targets23 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(NonEmptySubmit), @@ -1841,10 +1867,10 @@ describe("Machine", () => { states: { NonEmptyIdle: { on: { - NonEmptySubmit: (to) => - to.branch.done().resolve(({ event, target }) => - target.decoded(new NonEmptyDone({ requestId: event.value })) - ) + NonEmptySubmit: { + target: targets23.root.done, + decoded: ({ event }) => (new NonEmptyDone({ requestId: event.value })) + } } }, done: { @@ -2284,14 +2310,16 @@ describe("Machine", () => { it.effect("supports flat object states with path-aware handlers", () => Effect.gen(function*() { + const root24 = Machine.state({ + initial: "idle", + states: { + idle: Idle, + loading: Loading + } + }) + const targets24 = Machine.targets(root24) const machine = Machine.make({ - root: Machine.state({ - initial: "idle", - states: { - idle: Idle, - loading: Loading - } - }), + root: root24, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -2302,10 +2330,10 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.loading().resolve(({ event, state, target }) => - target.decoded(new Loading({ requestId: `${state.userId}:${event.value}` })) - ) + Submit: { + target: targets24.root.loading, + decoded: ({ event, state }) => (new Loading({ requestId: `${state.userId}:${event.value}` })) + } } } } @@ -2333,14 +2361,16 @@ describe("Machine", () => { it.effect("uses path identity for duplicate decoded state tags", () => Effect.gen(function*() { + const root25 = Machine.state({ + initial: "a", + states: { + a: Duplicate, + b: Duplicate + } + }) + const targets25 = Machine.targets(root25) const machine = Machine.make({ - root: Machine.state({ - initial: "a", - states: { - a: Duplicate, - b: Duplicate - } - }), + root: root25, events: Machine.eventsFromSchemas(Submit, Reset), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.a.decoded(new Duplicate({ value: "a" })))) @@ -2348,13 +2378,12 @@ describe("Machine", () => { states: { a: { on: { - Submit: (to) => - to.branch.b().resolve(({ event, target }) => target.decoded(new Duplicate({ value: event.value }))) + Submit: { target: targets25.root.b, decoded: ({ event }) => (new Duplicate({ value: event.value })) } } }, b: { on: { - Reset: (to) => to.branch.a().resolve(({ target }) => target.decoded(new Duplicate({ value: "reset" }))) + Reset: { target: targets25.root.a, decoded: () => (new Duplicate({ value: "reset" })) } } } } @@ -2384,14 +2413,16 @@ describe("Machine", () => { it.effect("exposes path identity through machine snapshots", () => Effect.gen(function*() { + const root26 = Machine.state({ + initial: "a", + states: { + a: Duplicate, + b: Duplicate + } + }) + const targets26 = Machine.targets(root26) const machine = Machine.make({ - root: Machine.state({ - initial: "a", - states: { - a: Duplicate, - b: Duplicate - } - }), + root: root26, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.a.decoded(new Duplicate({ value: "a" })))) @@ -2399,8 +2430,7 @@ describe("Machine", () => { states: { a: { on: { - Submit: (to) => - to.branch.b().resolve(({ event, target }) => target.decoded(new Duplicate({ value: event.value }))) + Submit: { target: targets26.root.b, decoded: ({ event }) => (new Duplicate({ value: event.value })) } } } } @@ -2420,17 +2450,19 @@ describe("Machine", () => { it.effect("honors final flat object state node configs", () => Effect.gen(function*() { - const machine = Machine.make({ - root: Machine.state({ - initial: "idle", - states: { - idle: Idle, - success: { - schema: Success, - type: "final" - } + const root27 = Machine.state({ + initial: "idle", + states: { + idle: Idle, + success: { + schema: Success, + type: "final" } - }), + } + }) + const targets27 = Machine.targets(root27) + const machine = Machine.make({ + root: root27, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.idle.decoded(new Idle({ userId: "user-1" })))) @@ -2438,10 +2470,10 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.success().resolve(({ event, target }) => - target.decoded(new Success({ requestId: event.value })) - ) + Submit: { + target: targets27.root.success, + decoded: ({ event }) => (new Success({ requestId: event.value })) + } } } } @@ -2463,21 +2495,25 @@ describe("Machine", () => { Effect.gen(function*() { const payment = new Payment({ id: "payment-1" }) const entering = new EnteringPayment({ amount: 100 }) + const root28 = Machine.state({ + initial: "payment", + states: { + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: AuthorizedPayment + } + }, + failed: Failed + } + }) + const targets28 = Machine.targets(root28) const machine = Machine.make({ - root: Machine.state({ - initial: "payment", - states: { - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: AuthorizedPayment - } - }, - failed: Failed - } - }), + branches: { transition2: { destination: { target: targets28.root.payment.authorized } } }, + + root: root28, events: Machine.eventsFromSchemas(Authorize), initialConfiguration: (to) => to.resolve(() => ({ @@ -2496,18 +2532,19 @@ describe("Machine", () => { states: { payment: { on: { - Authorize: (to) => - to.branch.failed().resolve(({ target }) => target.decoded(new Failed({ message: "parent" }))) + Authorize: { target: targets28.root.failed, decoded: () => (new Failed({ message: "parent" })) } }, states: { entering: { on: { - Authorize: (to) => - to.local.authorized().resolve(({ event, containingState, ancestors, target }) => { + Authorize: { + branches: "transition2", + resolve: ({ event, containingState, ancestors, select: { destination: target } }) => { assert.deepStrictEqual(containingState, payment) assert.deepStrictEqual(ancestors, { payment }) return target.decoded(new AuthorizedPayment({ code: event.code })) - }) + } + } } } } @@ -2546,7 +2583,10 @@ describe("Machine", () => { let declinableResolverCalls = 0 let raisedResolverCalls = 0 let lifecycleCalls = 0 + const targets29 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets29.root.Done } } }, + root: states, events: Machine.eventsFromSchemas(Check, Consume, Finish, Ignore), internalEvents: Machine.internalEventsFromSchemas(Raised), @@ -2558,28 +2598,37 @@ describe("Machine", () => { lifecycleCalls++ }, on: { - Check: (to) => - to.none.resolve(({ event, decline }, enqueue) => { + Check: { + none: true, + resolve: ({ event, decline }, enqueue) => { declinableResolverCalls++ if (!event.accept) return decline() enqueue.raise(new Raised({})) return undefined - }, { declinable: true }), - Consume: (to) => - to.none.resolve(() => { + }, + declinable: true + }, + Consume: { + none: true, + resolve: () => { requiredResolverCalls++ return undefined - }), - Finish: (to) => - to.branch.Done().resolve(({ target }) => { + } + }, + Finish: { + branches: "transition1", + resolve: ({ select: { destination: target } }) => { requiredResolverCalls++ return target.decoded(new Done({})) - }), - Raised: (to) => - to.none.resolve(() => { + } + }, + Raised: { + none: true, + resolve: () => { raisedResolverCalls++ return undefined - }) + } + } } }, Done: { @@ -2620,21 +2669,27 @@ describe("Machine", () => { class Notice extends Schema.TaggedClass("DeclineNotice")("Notice", {}) {} const payment = new Payment({ id: "payment-1" }) const entering = new EnteringPayment({ amount: 100 }) + const root30 = Machine.state({ + initial: "payment", + states: { + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: AuthorizedPayment + } + }, + failed: Failed + } + }) + const targets30 = Machine.targets(root30) const machine = Machine.make({ - root: Machine.state({ - initial: "payment", - states: { - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: AuthorizedPayment - } - }, - failed: Failed - } - }), + branches: { + transition2: { authorize: { target: targets30.root.payment.authorized }, consume: { none: true } } + }, + + root: root30, events: Machine.eventsFromSchemas(Authorize), emittedEvents: Machine.emittedEventsFromSchemas(Notice), initialConfiguration: (to) => @@ -2654,24 +2709,23 @@ describe("Machine", () => { states: { payment: { on: { - Authorize: (to) => - to.branch.failed().resolve(({ target }) => target.decoded(new Failed({ message: "parent" }))) + Authorize: { target: targets30.root.failed, decoded: () => (new Failed({ message: "parent" })) } }, states: { entering: { on: { - Authorize: (to) => - to.branches({ - authorize: { target: to.local.authorized() }, - consume: { target: to.none } - }).resolve(({ event, select, decline }, enqueue) => { + Authorize: { + branches: "transition2", + resolve: ({ event, select, decline }, enqueue) => { if (event.code === "child") { return select.authorize.decoded(new AuthorizedPayment({ code: event.code })) } if (event.code === "consume") return select.consume() enqueue.emit(new Notice({})) return decline() - }, { declinable: true }) + }, + declinable: true + } } } } @@ -2721,6 +2775,7 @@ describe("Machine", () => { finished: Finished } }) + const targets31 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(), @@ -2736,11 +2791,14 @@ describe("Machine", () => { }).handle({ states: { workflow: { - always: (to) => to.branch.finished().resolve(({ target }) => target.decoded(new Finished({}))), + always: { target: targets31.root.finished, decoded: () => (new Finished({})) }, states: { waiting: { - always: (to) => - to.none.resolve(({ state, decline }) => state.ready ? undefined : decline(), { declinable: true }) + always: { + none: true, + resolve: ({ state, decline }) => state.ready ? undefined : decline(), + declinable: true + } } } } @@ -2766,7 +2824,7 @@ describe("Machine", () => { states: { Stable: { on: { - Ping: (to) => to.none.resolve(({ decline }) => decline(), { declinable: true }) + Ping: { none: true, resolve: ({ decline }) => decline(), declinable: true } } } } @@ -2798,7 +2856,10 @@ describe("Machine", () => { finished: Finished } }) + const targets33 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets33.root.finished } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -2810,7 +2871,7 @@ describe("Machine", () => { }).handle({ states: { workflow: { - onDone: (to) => to.branch.finished().resolve(({ decline }) => decline(), { declinable: true }), + onDone: { branches: "transition1", resolve: ({ decline }) => decline(), declinable: true }, states: { complete: { output: () => "complete" } } @@ -2847,7 +2908,10 @@ describe("Machine", () => { } }) let parentCalls = 0 + const targets34 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets34.root.finished } } }, + root: states, events: Machine.eventsFromSchemas(Ping), initialConfiguration: (root) => @@ -2860,24 +2924,27 @@ describe("Machine", () => { states: { root: { on: { - Ping: (to) => - to.branch.finished().resolve(({ target }) => { + Ping: { + branches: "transition1", + resolve: ({ select: { destination: target } }) => { parentCalls++ return target.decoded(new Finished({})) - }) + } + } }, states: { left: { on: { - Ping: (to) => to.none.resolve(({ decline }) => decline(), { declinable: true }) + Ping: { none: true, resolve: ({ decline }) => decline(), declinable: true } } }, right: { on: { - Ping: (to) => - to.none.resolve(({ event, decline }) => event.handleRight ? undefined : decline(), { - declinable: true - }) + Ping: { + none: true, + resolve: ({ event, decline }) => event.handleRight ? undefined : decline(), + declinable: true + } } } } @@ -2905,25 +2972,27 @@ describe("Machine", () => { Effect.gen(function*() { const payment = new Payment({ id: "payment-1" }) const entering = new EnteringPayment({ amount: 100 }) - const machine = Machine.make({ - root: Machine.state({ - initial: "payment", - states: { - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: { - schema: AuthorizedPayment, - type: "final", - output: Schema.String - } + const root35 = Machine.state({ + initial: "payment", + states: { + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: { + schema: AuthorizedPayment, + type: "final", + output: Schema.String } - }, - failed: Failed - } - }), + } + }, + failed: Failed + } + }) + const targets35 = Machine.targets(root35) + const machine = Machine.make({ + root: root35, events: Machine.eventsFromSchemas(Authorize, Reset), initialConfiguration: (to) => to.resolve(() => ({ @@ -2942,16 +3011,15 @@ describe("Machine", () => { states: { payment: { on: { - Reset: (to) => - to.branch.failed().resolve(({ target }) => target.decoded(new Failed({ message: "reset" }))) + Reset: { target: targets35.root.failed, decoded: () => (new Failed({ message: "reset" })) } }, states: { entering: { on: { - Authorize: (to) => - to.local.authorized().resolve(({ event, target }) => - target.decoded(new AuthorizedPayment({ code: event.code })) - ) + Authorize: { + target: targets35.root.payment.authorized, + decoded: ({ event }) => (new AuthorizedPayment({ code: event.code })) + } } }, authorized: { @@ -2984,21 +3052,23 @@ describe("Machine", () => { Effect.gen(function*() { const payment = new Payment({ id: "payment-1" }) const entering = new EnteringPayment({ amount: 100 }) - const machine = Machine.make({ - root: Machine.state({ - initial: "idle", - states: { - idle: Idle, - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: AuthorizedPayment - } + const root36 = Machine.state({ + initial: "idle", + states: { + idle: Idle, + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: AuthorizedPayment } } - }), + } + }) + const targets36 = Machine.targets(root36) + const machine = Machine.make({ + root: root36, events: Machine.eventsFromSchemas(Reset), initialConfiguration: (to) => to.resolve(() => ({ @@ -3017,7 +3087,7 @@ describe("Machine", () => { states: { payment: { on: { - Reset: (to) => to.branch.idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-1" }))) + Reset: { target: targets36.root.idle, decoded: () => (new Idle({ userId: "user-1" })) } } } } @@ -3067,7 +3137,10 @@ describe("Machine", () => { } } }) + const targets37 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets37.root.fulfillment } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => @@ -3076,8 +3149,9 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.fulfillment().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( new Fulfillment({ id: event.value }), (fulfillment) => @@ -3092,7 +3166,7 @@ describe("Machine", () => { (shipping) => shipping.quoted.decoded(new ShippingQuoted({ quoteId: event.value })) ) ) - ) + } } } } @@ -3172,7 +3246,10 @@ describe("Machine", () => { } } }) + const targets38 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets38.root.workflow.fulfillment } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => @@ -3187,8 +3264,9 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.local.fulfillment().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( new Fulfillment({ id: event.value }), (fulfillment) => @@ -3203,7 +3281,7 @@ describe("Machine", () => { (shipping) => shipping.quoted.decoded(new ShippingQuoted({ quoteId: event.value })) ) ) - ) + } } } } @@ -3304,7 +3382,10 @@ describe("Machine", () => { } } } + const targets39 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets39.root.app.flow.fulfillment } } }, + root: states, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (to) => to.resolve(() => initial) @@ -3316,8 +3397,9 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.app.flow.fulfillment().resolve(({ event, target }) => + Submit: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( new Fulfillment({ id: event.value }), (fulfillment) => @@ -3325,7 +3407,7 @@ describe("Machine", () => { .inventory.decoded(new Inventory({ warehouse: "warehouse-1" })) .shipping.decoded(new Shipping({ address: "Main Street" })) ) - ) + } } } } @@ -3396,6 +3478,7 @@ describe("Machine", () => { const inventory = new Inventory({ warehouse: "warehouse-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) + const targets40 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(ReserveInventory), @@ -3422,10 +3505,10 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets40.root.fulfillment.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } } } @@ -3491,7 +3574,10 @@ describe("Machine", () => { const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) const nextInventory = new Inventory({ warehouse: "warehouse-2" }) + const targets41 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets41.root.fulfillment.inventory } } }, + root: states, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (root) => @@ -3517,14 +3603,15 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.branch.fulfillment.inventory().resolve(({ event, target }) => + ReserveInventory: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( nextInventory, (inventory) => inventory.reserved.decoded(new InventoryReserved({ reservationId: event.reservationId })) ) - ) + } } } } @@ -3591,7 +3678,10 @@ describe("Machine", () => { const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) const nextInventory = new Inventory({ warehouse: "warehouse-2" }) + const targets42 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets42.root.fulfillment.inventory } } }, + root: states, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (root) => @@ -3617,14 +3707,15 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.branch.fulfillment.inventory().resolve(({ event, target }) => + ReserveInventory: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( nextInventory, (inventory) => inventory.reserved.decoded(new InventoryReserved({ reservationId: event.reservationId })) ) - ) + } } } } @@ -3692,7 +3783,10 @@ describe("Machine", () => { const nextInventory = new Inventory({ warehouse: "warehouse-2" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) + const targets43 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets43.root.fulfillment } } }, + root: states, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (root) => @@ -3718,8 +3812,9 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.branch.fulfillment().resolve(({ event, target }) => + ReserveInventory: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( nextFulfillment, (fulfillment) => @@ -3731,7 +3826,7 @@ describe("Machine", () => { ) ) ) - ) + } } } } @@ -3796,7 +3891,10 @@ describe("Machine", () => { const payment = new Payment({ id: "payment-1" }) const inventory = new Inventory({ warehouse: "warehouse-1" }) const shipping = new Shipping({ address: "Main Street" }) + const targets44 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets44.root.payment.shipping } } }, + root: states, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (root) => @@ -3817,13 +3915,14 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.branch.payment.shipping().resolve(({ event, target }) => + ReserveInventory: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }) => target.decoded( shipping, (shipping) => shipping.quoted.decoded(new ShippingQuoted({ quoteId: event.reservationId })) ) - ) + } } } } @@ -3853,24 +3952,26 @@ describe("Machine", () => { it.effect("treats compound states as final when their active child is final", () => Effect.gen(function*() { const payment = new Payment({ id: "payment-1" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "payment", - states: { - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: { - schema: AuthorizedPayment, - type: "final", - output: Schema.String - } + const root45 = Machine.state({ + initial: "payment", + states: { + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: { + schema: AuthorizedPayment, + type: "final", + output: Schema.String } } } - }), + } + }) + const targets45 = Machine.targets(root45) + const machine = Machine.make({ + root: root45, events: Machine.eventsFromSchemas(Authorize, Reset), initialConfiguration: (to) => to.resolve(() => ({ @@ -3889,16 +3990,15 @@ describe("Machine", () => { states: { payment: { on: { - Reset: (to) => - to.local.entering().resolve(({ target }) => target.decoded(new EnteringPayment({ amount: 0 }))) + Reset: { target: targets45.root.payment.entering, decoded: () => (new EnteringPayment({ amount: 0 })) } }, states: { entering: { on: { - Authorize: (to) => - to.local.authorized().resolve(({ event, target }) => - target.decoded(new AuthorizedPayment({ code: event.code })) - ) + Authorize: { + target: targets45.root.payment.authorized, + decoded: ({ event }) => (new AuthorizedPayment({ code: event.code })) + } } }, authorized: { @@ -3982,25 +4082,27 @@ describe("Machine", () => { it.effect("joins with output from nested final completion and rejects later events", () => Effect.gen(function*() { const payment = new Payment({ id: "payment-1" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "idle", - states: { - idle: Idle, - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: { - schema: AuthorizedPayment, - type: "final", - output: Schema.String - } + const root46 = Machine.state({ + initial: "idle", + states: { + idle: Idle, + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: { + schema: AuthorizedPayment, + type: "final", + output: Schema.String } } } - }), + } + }) + const targets46 = Machine.targets(root46) + const machine = Machine.make({ + root: root46, events: Machine.eventsFromSchemas(Authorize, Reset), initialConfiguration: (to) => to.resolve(() => ({ @@ -4019,15 +4121,15 @@ describe("Machine", () => { states: { payment: { on: { - Reset: (to) => to.branch.idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-1" }))) + Reset: { target: targets46.root.idle, decoded: () => (new Idle({ userId: "user-1" })) } }, states: { entering: { on: { - Authorize: (to) => - to.local.authorized().resolve(({ event, target }) => - target.decoded(new AuthorizedPayment({ code: event.code })) - ) + Authorize: { + target: targets46.root.payment.authorized, + decoded: ({ event }) => (new AuthorizedPayment({ code: event.code })) + } } }, authorized: { @@ -4071,32 +4173,34 @@ describe("Machine", () => { Effect.gen(function*() { const checkout = new Fulfillment({ id: "checkout-1" }) const inventory = new Inventory({ warehouse: "warehouse-1" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "checkout", - states: { - checkout: { - schema: Fulfillment, - initial: "inventory", - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: { - schema: InventoryReserved, - type: "final", - output: Schema.String - } + const root47 = Machine.state({ + initial: "checkout", + states: { + checkout: { + schema: Fulfillment, + initial: "inventory", + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: { + schema: InventoryReserved, + type: "final", + output: Schema.String } - }, - shipped: ShippingQuoted - } - }, - failed: Failed - } - }), + } + }, + shipped: ShippingQuoted + } + }, + failed: Failed + } + }) + const targets47 = Machine.targets(root47) + const machine = Machine.make({ + root: root47, events: Machine.eventsFromSchemas(ReserveInventory, Reset), initialConfiguration: (to) => to.resolve(() => ({ @@ -4119,22 +4223,21 @@ describe("Machine", () => { states: { checkout: { on: { - Reset: (to) => - to.branch.failed().resolve(({ target }) => target.decoded(new Failed({ message: "reset" }))) + Reset: { target: targets47.root.failed, decoded: () => (new Failed({ message: "reset" })) } }, states: { inventory: { - onDone: (to) => - to.branch.checkout.shipped().resolve(({ output, target }) => - target.decoded(new ShippingQuoted({ quoteId: String(output) })) - ), + onDone: { + target: targets47.root.checkout.shipped, + decoded: ({ output }) => (new ShippingQuoted({ quoteId: String(output) })) + }, states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets47.root.checkout.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } }, reserved: { @@ -4165,41 +4268,43 @@ describe("Machine", () => { const inventory = new Inventory({ warehouse: "warehouse-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: { - schema: InventoryReserved, - type: "final" - } + const root48 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: { + schema: InventoryReserved, + type: "final" } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping, - quoted: { - schema: ShippingQuoted, - type: "final", - output: Schema.String - } + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping, + quoted: { + schema: ShippingQuoted, + type: "final", + output: Schema.String } } } } } - }), + } + }) + const targets48 = Machine.targets(root48) + const machine = Machine.make({ + root: root48, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (to) => to.resolve(() => ({ @@ -4236,10 +4341,10 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets48.root.fulfillment.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } } } @@ -4287,46 +4392,48 @@ describe("Machine", () => { const checking = new CheckingInventory({ sku: "sku-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - output: Schema.Struct({ - inventory: Schema.String, - shipping: Schema.String - }), - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: { - schema: InventoryReserved, - type: "final", - output: Schema.String - } + const root49 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + output: Schema.Struct({ + inventory: Schema.String, + shipping: Schema.String + }), + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: { + schema: InventoryReserved, + type: "final", + output: Schema.String } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping, - quoted: { - schema: ShippingQuoted, - type: "final", - output: Schema.String - } + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping, + quoted: { + schema: ShippingQuoted, + type: "final", + output: Schema.String } } } } } - }), + } + }) + const targets49 = Machine.targets(root49) + const machine = Machine.make({ + root: root49, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (to) => to.resolve(() => ({ @@ -4367,10 +4474,10 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets49.root.fulfillment.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } }, reserved: { @@ -4382,10 +4489,10 @@ describe("Machine", () => { states: { quoting: { on: { - ReserveInventory: (to) => - to.local.quoted().resolve(({ event, target }) => - target.decoded(new ShippingQuoted({ quoteId: event.reservationId })) - ) + ReserveInventory: { + target: targets49.root.fulfillment.shipping.quoted, + decoded: ({ event }) => (new ShippingQuoted({ quoteId: event.reservationId })) + } } }, quoted: { @@ -4441,46 +4548,48 @@ describe("Machine", () => { const inventory = new Inventory({ warehouse: "warehouse-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - output: Schema.Struct({ - inventory: Schema.String, - shipping: Schema.String - }), - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: { - schema: InventoryReserved, - type: "final", - output: Schema.String - } + const root50 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + output: Schema.Struct({ + inventory: Schema.String, + shipping: Schema.String + }), + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: { + schema: InventoryReserved, + type: "final", + output: Schema.String } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping, - quoted: { - schema: ShippingQuoted, - type: "final", - output: Schema.String - } + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping, + quoted: { + schema: ShippingQuoted, + type: "final", + output: Schema.String } } } } } - }), + } + }) + const targets50 = Machine.targets(root50) + const machine = Machine.make({ + root: root50, events: Machine.eventsFromSchemas(ReserveInventory, Resolve), initialConfiguration: (to) => to.resolve(() => ({ @@ -4518,10 +4627,10 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets50.root.fulfillment.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } }, reserved: { @@ -4533,10 +4642,10 @@ describe("Machine", () => { states: { quoting: { on: { - Resolve: (to) => - to.local.quoted().resolve(({ target }) => - target.decoded(new ShippingQuoted({ quoteId: "quote-1" })) - ) + Resolve: { + target: targets50.root.fulfillment.shipping.quoted, + decoded: () => (new ShippingQuoted({ quoteId: "quote-1" })) + } } }, quoted: { @@ -4591,34 +4700,36 @@ describe("Machine", () => { const checking = new CheckingInventory({ sku: "sku-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: InventoryReserved - } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping, - quoted: ShippingQuoted - } + const root51 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: InventoryReserved + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping, + quoted: ShippingQuoted } } } } - }), + } + }) + const targets51 = Machine.targets(root51) + const machine = Machine.make({ + root: root51, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (to) => to.resolve(() => ({ @@ -4655,10 +4766,10 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }) => - target.decoded(new InventoryReserved({ reservationId: event.reservationId })) - ) + ReserveInventory: { + target: targets51.root.fulfillment.inventory.reserved, + decoded: ({ event }) => (new InventoryReserved({ reservationId: event.reservationId })) + } } } } @@ -4667,10 +4778,10 @@ describe("Machine", () => { states: { quoting: { on: { - ReserveInventory: (to) => - to.local.quoted().resolve(({ event, target }) => - target.decoded(new ShippingQuoted({ quoteId: event.reservationId })) - ) + ReserveInventory: { + target: targets51.root.fulfillment.shipping.quoted, + decoded: ({ event }) => (new ShippingQuoted({ quoteId: event.reservationId })) + } } } } @@ -4710,34 +4821,38 @@ describe("Machine", () => { const checking = new CheckingInventory({ sku: "sku-1" }) const shipping = new Shipping({ address: "Main Street" }) const quoting = new QuotingShipping({ postalCode: "12345" }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory, - reserved: InventoryReserved - } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping, - quoted: ShippingQuoted - } + const root52 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory, + reserved: InventoryReserved + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping, + quoted: ShippingQuoted } } } } - }), + } + }) + const targets52 = Machine.targets(root52) + const machine = Machine.make({ + branches: { transition1: { destination: { target: targets52.root.fulfillment.inventory.reserved } } }, + + root: root52, events: Machine.eventsFromSchemas(ReserveInventory, Resolve), initialConfiguration: (to) => to.resolve(() => ({ @@ -4774,15 +4889,17 @@ describe("Machine", () => { states: { checking: { on: { - ReserveInventory: (to) => - to.local.reserved().resolve(({ event, target }, enqueue) => { + ReserveInventory: { + branches: "transition1", + resolve: ({ event, select: { destination: target } }, enqueue) => { enqueue.raise(new Resolve({})) return target.decoded( new InventoryReserved({ reservationId: event.reservationId }) ) - }) + } + } } } } @@ -4791,10 +4908,10 @@ describe("Machine", () => { states: { quoting: { on: { - Resolve: (to) => - to.local.quoted().resolve(({ target }) => - target.decoded(new ShippingQuoted({ quoteId: "raised" })) - ) + Resolve: { + target: targets52.root.fulfillment.shipping.quoted, + decoded: () => (new ShippingQuoted({ quoteId: "raised" })) + } } } } @@ -4844,8 +4961,10 @@ describe("Machine", () => { it.effect("handlers can return snapshots directly", () => Effect.gen(function*() { + const root53 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets53 = Machine.targets(root53) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root53, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -4856,8 +4975,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets53.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -4882,8 +5000,10 @@ describe("Machine", () => { })) it("enabled returns the event tags handled by the current state", () => { + const root54 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets54 = Machine.targets(root54) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root54, events: Machine.eventsFromSchemas(Submit, Reset), input: Input, initialConfiguration: (root) => @@ -4894,13 +5014,12 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets54.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { on: { - Reset: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-1" }))) + Reset: { target: targets54.root.Idle, decoded: () => (new Idle({ userId: "user-1" })) } } } } @@ -4925,14 +5044,16 @@ describe("Machine", () => { }) it("enabled returns no event tags for final states", () => { + const root55 = Machine.state({ + initial: "Idle", + states: { + Idle, + Success: { schema: Success, type: "final" } + } + }) + const targets55 = Machine.targets(root55) const machine = Machine.make({ - root: Machine.state({ - initial: "Idle", - states: { - Idle, - Success: { schema: Success, type: "final" } - } - }), + root: root55, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -4943,8 +5064,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))) + Submit: { target: targets55.root.Success, decoded: () => (new Success({ requestId: "request-1" })) } } }, Success: {} @@ -4963,8 +5083,10 @@ describe("Machine", () => { it.effect("exposes final state output from a running machine", () => Effect.gen(function*() { + const root56 = Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }) + const targets56 = Machine.targets(root56) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }), + root: root56, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -4975,8 +5097,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))) + Submit: { target: targets56.root.Success, decoded: () => (new Success({ requestId: "request-1" })) } } }, Success: { @@ -5003,8 +5124,10 @@ describe("Machine", () => { it.effect("plans final state output without running deferred actions", () => Effect.gen(function*() { + const root57 = Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }) + const targets57 = Machine.targets(root57) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }), + root: root57, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5015,8 +5138,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))) + Submit: { target: targets57.root.Success, decoded: () => (new Success({ requestId: "request-1" })) } } }, Success: { @@ -5110,14 +5232,16 @@ describe("Machine", () => { it.effect("defaults final state output to undefined", () => Effect.gen(function*() { + const root58 = Machine.state({ + initial: "Idle", + states: { + Idle, + Success: { schema: Success, type: "final" } + } + }) + const targets58 = Machine.targets(root58) const machine = Machine.make({ - root: Machine.state({ - initial: "Idle", - states: { - Idle, - Success: { schema: Success, type: "final" } - } - }), + root: root58, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5128,8 +5252,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))) + Submit: { target: targets58.root.Success, decoded: () => (new Success({ requestId: "request-1" })) } } }, Success: {} @@ -5157,14 +5280,16 @@ describe("Machine", () => { it.effect("rejects events after reaching a final state", () => Effect.gen(function*() { + const root59 = Machine.state({ + initial: "Idle", + states: { + Idle, + Success: { schema: Success, type: "final" } + } + }) + const targets59 = Machine.targets(root59) const machine = Machine.make({ - root: Machine.state({ - initial: "Idle", - states: { - Idle, - Success: { schema: Success, type: "final" } - } - }), + root: root59, events: Machine.eventsFromSchemas(Submit, Reset), input: Input, initialConfiguration: (root) => @@ -5175,9 +5300,8 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))), - Reset: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-2" }))) + Submit: { target: targets59.root.Success, decoded: () => (new Success({ requestId: "request-1" })) }, + Reset: { target: targets59.root.Idle, decoded: () => (new Idle({ userId: "user-2" })) } } }, Success: {} @@ -5206,8 +5330,10 @@ describe("Machine", () => { it.effect("start keeps the machine alive after the starting fiber completes", () => Effect.gen(function*() { + const root60 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets60 = Machine.targets(root60) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root60, events: Machine.eventsFromSchemas(Submit), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.decoded(new Idle({ userId: "user-1" })))) @@ -5215,8 +5341,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets60.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -5237,8 +5362,10 @@ describe("Machine", () => { it.effect("start rejects events sent after stop", () => Effect.gen(function*() { + const root61 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets61 = Machine.targets(root61) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root61, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5249,8 +5376,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets61.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -5309,8 +5435,9 @@ describe("Machine", () => { it.effect("handlers use target.none for explicit targetless transitions", () => Effect.gen(function*() { + const root62 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root62, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5321,7 +5448,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => to.none + Submit: { none: true } } } } @@ -5337,8 +5464,10 @@ describe("Machine", () => { it.effect("start returns a machine runtime with lifecycle snapshots", () => Effect.gen(function*() { + const root63 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets63 = Machine.targets(root63) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root63, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5349,8 +5478,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets63.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -5399,8 +5527,10 @@ describe("Machine", () => { it.effect("start completes machine output from a final state", () => Effect.gen(function*() { + const root64 = Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }) + const targets64 = Machine.targets(root64) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Success: SuccessOutput } }), + root: root64, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -5411,8 +5541,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))) + Submit: { target: targets64.root.Success, decoded: () => (new Success({ requestId: "request-1" })) } } }, Success: { @@ -5443,9 +5572,11 @@ describe("Machine", () => { it.effect("plan ignores events without an enabled transition", () => Effect.gen(function*() { + const root65 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets65 = Machine.targets(root65) const machine = Machine.make({ id: "UserMachine", - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root65, events: Machine.eventsFromSchemas(Submit, Reset), input: Input, initialConfiguration: (root) => @@ -5456,8 +5587,7 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets65.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } } } @@ -5474,8 +5604,12 @@ describe("Machine", () => { it.effect("start runs invoke configs", () => Effect.gen(function*() { + const root66 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets66 = Machine.targets(root66) const definition = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + effects: { source1: Effect.suspend(() => Effect.succeed("done:request-1")) }, + + root: root66, events: Machine.eventsFromSchemas(Submit, RequestSucceeded), input: Input, initialConfiguration: (root) => @@ -5487,15 +5621,15 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets66.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.effect("request", () => Effect.succeed("done:request-1")).onDone((to) => - to.branch.Success().resolve(({ output, target }) => target.decoded(new Success({ requestId: output }))) - ) + invoke: { + src: "source1", + id: "request", + onDone: { target: targets66.root.Success, decoded: ({ output }) => (new Success({ requestId: output })) } + } }, Success: { output: ({ state }) => state.requestId @@ -5528,8 +5662,12 @@ describe("Machine", () => { it.effect("start invokes a child process and handles its output event", () => Effect.gen(function*() { + const root67 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets67 = Machine.targets(root67) const definition = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + effects: { source1: Effect.suspend(() => Effect.succeed("done:request-1")) }, + + root: root67, events: Machine.eventsFromSchemas(Submit, RequestSucceeded), input: Input, initialConfiguration: (root) => @@ -5541,15 +5679,15 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets67.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.effect("request", () => Effect.succeed("done:request-1")).onDone((to) => - to.branch.Success().resolve(({ output, target }) => target.decoded(new Success({ requestId: output }))) - ) + invoke: { + src: "source1", + id: "request", + onDone: { target: targets67.root.Success, decoded: ({ output }) => (new Success({ requestId: output })) } + } }, Success: { output: ({ state }) => state.requestId @@ -5592,6 +5730,8 @@ describe("Machine", () => { const Child = Machine.child("shared-child", childMachine) const parentStates = Machine.state({ initial: "Loading", states: { Loading } }) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -5599,7 +5739,7 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => from.child(Child) + invoke: { src: "source1" } } } }) @@ -5655,13 +5795,15 @@ describe("Machine", () => { const Child = Machine.child("owned-child", childMachine) const parentStates = Machine.state({ initial: "Loading", states: { Loading } }) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.decoded(new Loading({ requestId: "parent" })))) }).handle({ states: { - Loading: { invoke: (from) => from.child(Child) } + Loading: { invoke: { src: "source1" } } } }) @@ -5700,6 +5842,8 @@ describe("Machine", () => { const Child = Machine.child("input-child", childMachine) const parentStates = Machine.state({ initial: "Loading", states: { Loading } }) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -5707,7 +5851,7 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => from.child(Child, { input: { userId: "configured" } }) + invoke: { src: "source1", input: () => ({ userId: "configured" }) } } } }) @@ -5760,7 +5904,10 @@ describe("Machine", () => { Success: { schema: Success, type: "final", output: Schema.String } } }) + const targets71 = Machine.targets(parentStates) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: Machine.eventsFromSchemas(ChildFinished), initialConfiguration: (root) => @@ -5768,10 +5915,10 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.child(Child).onDone((to) => - to.branch.Success().resolve(({ output, target }) => target.decoded(new Success({ requestId: output }))) - ) + invoke: { + src: "source1", + onDone: { target: targets71.root.Success, decoded: ({ output }) => (new Success({ requestId: output })) } + } }, Success: { output: ({ state }) => state.requestId } } @@ -5829,6 +5976,8 @@ describe("Machine", () => { const Child = Machine.child("child-machine", child) const parentStates = Machine.state({ initial: "Loading", states: { Loading } }) const parent = Machine.make({ + children: { source1: Child, source2: Child }, + root: parentStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -5836,7 +5985,7 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => [from.child(Child), from.child(Child)] + invoke: [{ src: "source1" }, { src: "source2" }] } } }) @@ -5852,12 +6001,14 @@ describe("Machine", () => { const First = Machine.childAddress("first") const Second = Machine.childAddress("second") let sourceEvaluations = 0 - const source = () => { + const source = (_input: undefined) => { sourceEvaluations += 1 return Machine.logic({ initial: undefined, run: () => Effect.never }) } const parentStates = Machine.state({ initial: "Loading", states: { Loading } }) const parent = Machine.make({ + logic: { source1: source, source2: source }, + root: parentStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -5865,12 +6016,12 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: ( - from - ) => [ - from.logic("worker", { address: First, logic: source }), - from.logic("worker", { address: Second, logic: source }) - ] + invoke: [{ src: "source1", id: "worker", address: First, input: () => undefined }, { + src: "source2", + id: "worker", + address: Second, + input: () => undefined + }] } } }) @@ -5886,8 +6037,12 @@ describe("Machine", () => { it.effect("start maps invoked child failures to machine events", () => Effect.gen(function*() { const error = new InvokeError({ message: "boom" }) + const root74 = Machine.state({ initial: "Idle", states: { Idle, Loading, Failed: FailedOutput } }) + const targets74 = Machine.targets(root74) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Failed: FailedOutput } }), + effects: { source1: Effect.suspend(() => Effect.fail(error)) }, + + root: root74, events: Machine.eventsFromSchemas(Submit, RequestFailed), input: Input, initialConfiguration: (root) => @@ -5898,17 +6053,18 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets74.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.effect("request", () => Effect.fail(error)).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ message: error.message })) - ) - ) + invoke: { + src: "source1", + id: "request", + onFailure: { + target: targets74.root.Failed, + decoded: ({ error }) => (new Failed({ message: error.message })) + } + } }, Failed: { output: ({ state }) => state.message @@ -5938,8 +6094,12 @@ describe("Machine", () => { it.effect("start delivers internal invoke events without exposing them through send", () => Effect.gen(function*() { + const root75 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets75 = Machine.targets(root75) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + effects: { source1: Effect.suspend(() => Effect.succeed("loaded")) }, + + root: root75, events: Machine.eventsFromSchemas(Submit), internalEvents: Machine.internalEventsFromSchemas(RequestSucceeded), initialConfiguration: (root) => @@ -5948,15 +6108,15 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets75.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.effect("request", () => Effect.succeed("loaded")).onDone((to) => - to.branch.Success().resolve(({ output, target }) => target.decoded(new Success({ requestId: output }))) - ) + invoke: { + src: "source1", + id: "request", + onDone: { target: targets75.root.Success, decoded: ({ output }) => (new Success({ requestId: output })) } + } }, Success: { output: ({ state }) => state.requestId @@ -5973,33 +6133,40 @@ describe("Machine", () => { it.effect("routes sendTo(parent) from an active invoked machine", () => Effect.gen(function*() { const childStarted = yield* Deferred.make() + const root76 = Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }) + const targets76 = Machine.targets(root76) const machine = Machine.make({ - root: Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }), + logic: { + source1: Machine.logic({ + initial: undefined, + run: ({ parent, sendTo }) => + parent === undefined ? + Effect.die("child expected an owning actor") : + Deferred.succeed(childStarted, void 0).pipe( + Effect.andThen(sendTo(parent, new RequestSucceeded({ value: "child" }))), + Effect.andThen(Effect.never) + ) + }) + }, + + root: root76, events: Machine.eventsFromSchemas(RequestSucceeded), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.decoded(new Loading({ requestId: "request-1" })))) }).handle({ states: { Loading: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("request-parent"), - logic: Machine.logic({ - initial: undefined, - run: ({ parent, sendTo }) => - parent === undefined ? - Effect.die("child expected an owning actor") : - Deferred.succeed(childStarted, void 0).pipe( - Effect.andThen(sendTo(parent, new RequestSucceeded({ value: "child" }))), - Effect.andThen(Effect.never) - ) - }) - }).onFailure((to) => to.none), + invoke: { + src: "source1", + id: "request", + address: Machine.childAddress("request-parent"), + onFailure: { none: true } + }, on: { - RequestSucceeded: (to) => - to.branch.Success().resolve(({ event, target }) => - target.decoded(new Success({ requestId: event.value })) - ) + RequestSucceeded: { + target: targets76.root.Success, + decoded: ({ event }) => (new Success({ requestId: event.value })) + } } }, Success: { @@ -6017,8 +6184,23 @@ describe("Machine", () => { it.effect("drops sendTo(parent) from a stale invoked machine finalizer", () => Effect.gen(function*() { const childStarted = yield* Deferred.make() + const root77 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets77 = Machine.targets(root77) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + logic: { + source1: Machine.logic({ + initial: undefined, + run: ({ parent, sendTo }) => + parent === undefined ? + Effect.die("child expected an owning actor") : + Deferred.succeed(childStarted, void 0).pipe( + Effect.andThen(Effect.never), + Effect.onInterrupt(() => sendTo(parent, new RequestSucceeded({ value: "stale" }))) + ) + }) + }, + + root: root77, events: Machine.eventsFromSchemas(Resolve, RequestSucceeded), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Loading.decoded(new Loading({ requestId: "request-1" })))) @@ -6026,30 +6208,21 @@ describe("Machine", () => { states: { Idle: { on: { - RequestSucceeded: (to) => - to.branch.Success().resolve(({ event, target }) => - target.decoded(new Success({ requestId: event.value })) - ) + RequestSucceeded: { + target: targets77.root.Success, + decoded: ({ event }) => (new Success({ requestId: event.value })) + } } }, Loading: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("stale-request"), - logic: Machine.logic({ - initial: undefined, - run: ({ parent, sendTo }) => - parent === undefined ? - Effect.die("child expected an owning actor") : - Deferred.succeed(childStarted, void 0).pipe( - Effect.andThen(Effect.never), - Effect.onInterrupt(() => sendTo(parent, new RequestSucceeded({ value: "stale" }))) - ) - }) - }).onFailure((to) => to.none), + invoke: { + src: "source1", + id: "request", + address: Machine.childAddress("stale-request"), + onFailure: { none: true } + }, on: { - Resolve: (to) => - to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({ userId: "resolved" }))) + Resolve: { target: targets77.root.Idle, decoded: () => (new Idle({ userId: "resolved" })) } } }, Success: { @@ -6081,8 +6254,12 @@ describe("Machine", () => { it.effect("inline Effect invocation handles typed failures without manual recovery", () => Effect.gen(function*() { const failure = new InvokeError({ message: "unavailable" }) + const root78 = Machine.state({ initial: "Loading", states: { Loading, Failed: FailedOutput } }) + const targets78 = Machine.targets(root78) const machine = Machine.make({ - root: Machine.state({ initial: "Loading", states: { Loading, Failed: FailedOutput } }), + effects: { source1: Effect.suspend(() => Effect.fail(failure)) }, + + root: root78, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(RequestSucceeded, RequestFailed), initialConfiguration: (root) => @@ -6090,12 +6267,14 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.effect("request", () => Effect.fail(failure)).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ message: error.message })) - ) - ) + invoke: { + src: "source1", + id: "request", + onFailure: { + target: targets78.root.Failed, + decoded: ({ error }) => (new Failed({ message: error.message })) + } + } }, Failed: { output: ({ state }) => state.message @@ -6113,8 +6292,12 @@ describe("Machine", () => { const requiredMessage: Effect.Effect = Effect.gen(function*() { return (yield* InitialRequirement).initialMessage }) + const root79 = Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }) + const targets79 = Machine.targets(root79) const machine = Machine.make({ - root: Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }), + effects: { source1: Effect.suspend(() => requiredMessage) }, + + root: root79, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(RequestSucceeded), initialConfiguration: (root) => @@ -6122,10 +6305,11 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.effect("request", () => requiredMessage).onDone((to) => - to.branch.Success().resolve(({ output, target }) => target.decoded(new Success({ requestId: output }))) - ) + invoke: { + src: "source1", + id: "request", + onDone: { target: targets79.root.Success, decoded: ({ output }) => (new Success({ requestId: output })) } + } }, Success: { output: ({ state }) => state.requestId @@ -6145,8 +6329,12 @@ describe("Machine", () => { it.effect("after emits a state-scoped internal event", () => Effect.gen(function*() { + const root80 = Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }) + const targets80 = Machine.targets(root80) const machine = Machine.make({ - root: Machine.state({ initial: "Loading", states: { Loading, Success: SuccessOutput } }), + timers: { source1: "1 hour" }, + + root: root80, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(RequestSucceeded), initialConfiguration: (root) => @@ -6154,10 +6342,11 @@ describe("Machine", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.timer("timeout", "1 hour").onDone((to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "timeout" }))) - ) + invoke: { + src: "source1", + id: "timeout", + onDone: { target: targets80.root.Success, decoded: () => (new Success({ requestId: "timeout" })) } + } }, Success: { output: ({ state }) => state.requestId @@ -6174,8 +6363,12 @@ describe("Machine", () => { it.effect("start maps invoked child active snapshots to machine events", () => Effect.gen(function*() { + const root81 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets81 = Machine.targets(root81) const definition = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + logic: { progress: Machine.logic({ initial: "pending", run: () => Effect.never }) }, + + root: root81, events: Machine.eventsFromSchemas(Submit, RequestProgress), input: Input, initialConfiguration: (root) => @@ -6183,40 +6376,23 @@ describe("Machine", () => { target.from((to) => to.Idle.decoded(new Idle({ userId: input.userId }))) ) }) - const onSnapshot: Machine.Machine.InvokeTransition< - Machine.Machine.States, - Machine.Machine.Events, - Machine.Machine.EmittedEvents, - "Loading", - Machine.Machine.InvokeSnapshotContext< - Machine.Machine.States, - Machine.Machine.Events, - Machine.Machine.EmittedEvents, - "Loading", - string, - never, - never, - Machine.Machine.InputEvents, - Machine.Machine.ParentEvents - > - > = (to) => - to.branch.Success().resolve(({ id, snapshot, target }) => - target.decoded(new Success({ requestId: `${id}:${snapshot.state}` })) - ) const machine = definition.handle({ states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets81.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("progress-request"), - logic: Machine.logic({ initial: "pending", run: () => Effect.never }) - }).onSnapshot(onSnapshot) + invoke: { + src: "progress", + id: "request", + address: Machine.childAddress("progress-request"), + onSnapshot: { + target: targets81.root.Success, + decoded: ({ id, snapshot }) => new Success({ requestId: `${id}:${snapshot.state}` }) + } + } }, Success: { output: ({ state }) => state.requestId @@ -6250,8 +6426,12 @@ describe("Machine", () => { it.effect("start fails the owning machine when an invoked effect defects", () => Effect.gen(function*() { const error = new InvokeError({ message: "boom" }) + const root82 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets82 = Machine.targets(root82) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + effects: { source1: Effect.suspend(() => Effect.die(error)) }, + + root: root82, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -6262,12 +6442,11 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets82.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => from.effect("request", () => Effect.die(error)) + invoke: { src: "source1", id: "request" } } } }) @@ -6291,8 +6470,25 @@ describe("Machine", () => { Effect.gen(function*() { const started = yield* Deferred.make() const release = yield* Deferred.make() + const root83 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets83 = Machine.targets(root83) const definition = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + logic: { + progress: Machine.logic({ + initial: "pending", + run: ({ setState }) => + Deferred.succeed(started, void 0).pipe( + Effect.andThen(Deferred.await(release)), + Effect.andThen(setState("ready")), + Effect.andThen(Effect.never) + ) + }) + }, + branches: { + snapshots: { ready: { target: targets83.root.Success, title: "Request is ready" }, unchanged: { none: true } } + }, + + root: root83, events: Machine.eventsFromSchemas(Submit, RequestProgress), input: Input, initialConfiguration: (root) => @@ -6300,53 +6496,26 @@ describe("Machine", () => { target.from((to) => to.Idle.decoded(new Idle({ userId: input.userId }))) ) }) - const onSnapshot: Machine.Machine.InvokeTransition< - Machine.Machine.States, - Machine.Machine.Events, - Machine.Machine.EmittedEvents, - "Loading", - Machine.Machine.InvokeSnapshotContext< - Machine.Machine.States, - Machine.Machine.Events, - Machine.Machine.EmittedEvents, - "Loading", - string, - never, - never, - Machine.Machine.InputEvents, - Machine.Machine.ParentEvents - > - > = (to) => - to.branches({ - ready: { title: "Request is ready", target: to.branch.Success() }, - unchanged: { target: to.none } - }).resolve(({ snapshot, select }) => - snapshot.state === "ready" - ? select.ready.decoded(new Success({ requestId: snapshot.state })) - : select.unchanged() - ) const machine = definition.handle({ states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets83.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("filtered-progress"), - logic: Machine.logic({ - initial: "pending", - run: ({ setState }) => - Deferred.succeed(started, void 0).pipe( - Effect.andThen(Deferred.await(release)), - Effect.andThen(setState("ready")), - Effect.andThen(Effect.never) - ) - }) - }).onSnapshot(onSnapshot) + invoke: { + src: "progress", + id: "request", + address: Machine.childAddress("filtered-progress"), + onSnapshot: { + branches: "snapshots", + resolve: ({ snapshot, select }) => + snapshot.state === "ready" + ? select.ready.decoded(new Success({ requestId: snapshot.state })) + : select.unchanged() + } + } }, Success: { output: ({ state }) => state.requestId @@ -6389,8 +6558,17 @@ describe("Machine", () => { it.effect("start allows invoked children without a snapshot handler", () => Effect.gen(function*() { + const root84 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets84 = Machine.targets(root84) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + logic: { + source1: Machine.logic({ + initial: "pending", + run: () => Effect.void + }) + }, + + root: root84, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -6401,19 +6579,16 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets84.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("void-request"), - logic: Machine.logic({ - initial: "pending", - run: () => Effect.void - }) - }).onDone((to) => to.none) + invoke: { + src: "source1", + id: "request", + address: Machine.childAddress("void-request"), + onDone: { none: true } + } } } }) @@ -6453,8 +6628,12 @@ describe("Machine", () => { ) ) }) + const root85 = Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }) + const targets85 = Machine.targets(root85) const machine = Machine.make({ - root: Machine.state({ initial: "Idle", states: { Idle, Loading, Success: SuccessOutput } }), + logic: { source1: childLogic }, + + root: root85, events: Machine.eventsFromSchemas(Submit, Resolve, RequestSucceeded), input: Input, initialConfiguration: (root) => @@ -6465,20 +6644,17 @@ describe("Machine", () => { states: { Idle: { on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets85.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - invoke: (from) => - from.logic("request", { address: Machine.childAddress("stopping-request"), logic: childLogic }), + invoke: { src: "source1", id: "request", address: Machine.childAddress("stopping-request") }, on: { - Resolve: (to) => - to.branch.Success().resolve(({ target }) => target.decoded(new Success({ requestId: "request-1" }))), - RequestSucceeded: (to) => - to.branch.Success().resolve(({ event, target }) => - target.decoded(new Success({ requestId: event.value })) - ) + Resolve: { target: targets85.root.Success, decoded: () => (new Success({ requestId: "request-1" })) }, + RequestSucceeded: { + target: targets85.root.Success, + decoded: ({ event }) => (new Success({ requestId: event.value })) + } } }, Success: { @@ -6543,20 +6719,28 @@ describe("Machine", () => { Effect.onInterrupt(() => Ref.update(stopped, (labels) => [...labels, label])) ) }) - const machine = Machine.make({ - root: Machine.state({ - initial: "payment", - states: { - payment: { - schema: Payment, - initial: "entering", - states: { - entering: EnteringPayment, - authorized: AuthorizedPayment - } + const root86 = Machine.state({ + initial: "payment", + states: { + payment: { + schema: Payment, + initial: "entering", + states: { + entering: EnteringPayment, + authorized: AuthorizedPayment } } - }), + } + }) + const targets86 = Machine.targets(root86) + const machine = Machine.make({ + logic: { + source1: makeInvokeLogic("parent", parentStarted), + source2: makeInvokeLogic("entering", enteringStarted), + source3: makeInvokeLogic("authorized", authorizedStarted) + }, + + root: root86, events: Machine.eventsFromSchemas(Authorize), initialConfiguration: (to) => to.resolve(() => ({ @@ -6574,35 +6758,23 @@ describe("Machine", () => { }).handle({ states: { payment: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("payment-parent"), - logic: makeInvokeLogic("parent", parentStarted) - }), + invoke: { src: "source1", id: "request", address: Machine.childAddress("payment-parent") }, states: { entering: { entry: ({ ancestors, state }) => { assert.deepStrictEqual(state, entering) assert.deepStrictEqual(ancestors, { payment }) }, - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("payment-entering"), - logic: makeInvokeLogic("entering", enteringStarted) - }), + invoke: { src: "source2", id: "request", address: Machine.childAddress("payment-entering") }, on: { - Authorize: (to) => - to.local.authorized().resolve(({ event, target }) => - target.decoded(new AuthorizedPayment({ code: event.code })) - ) + Authorize: { + target: targets86.root.payment.authorized, + decoded: ({ event }) => (new AuthorizedPayment({ code: event.code })) + } } }, authorized: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("payment-authorized"), - logic: makeInvokeLogic("authorized", authorizedStarted) - }) + invoke: { src: "source3", id: "request", address: Machine.childAddress("payment-authorized") } } } } @@ -6657,37 +6829,45 @@ describe("Machine", () => { ) ) }) - const machine = Machine.make({ - root: Machine.state({ - initial: "fulfillment", - states: { - fulfillment: { - schema: Fulfillment, - type: "parallel", - states: { - inventory: { - schema: Inventory, - initial: "checking", - states: { - checking: CheckingInventory - } - }, - shipping: { - schema: Shipping, - initial: "quoting", - states: { - quoting: QuotingShipping - } + const root87 = Machine.state({ + initial: "fulfillment", + states: { + fulfillment: { + schema: Fulfillment, + type: "parallel", + states: { + inventory: { + schema: Inventory, + initial: "checking", + states: { + checking: CheckingInventory + } + }, + shipping: { + schema: Shipping, + initial: "quoting", + states: { + quoting: QuotingShipping } } - }, - success: { - schema: Success, - type: "final", - output: Schema.String } + }, + success: { + schema: Success, + type: "final", + output: Schema.String } - }), + } + }) + const targets87 = Machine.targets(root87) + const machine = Machine.make({ + logic: { + source1: makeInvokeLogic(parentStarted, parentStopping), + source2: makeInvokeLogic(inventoryStarted, inventoryStopping), + source3: makeInvokeLogic(shippingStarted, shippingStopping) + }, + + root: root87, events: Machine.eventsFromSchemas(ReserveInventory), initialConfiguration: (to) => to.resolve(() => ({ @@ -6719,33 +6899,23 @@ describe("Machine", () => { }).handle({ states: { fulfillment: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("fulfillment-parent"), - logic: makeInvokeLogic(parentStarted, parentStopping) - }), + invoke: { src: "source1", id: "request", address: Machine.childAddress("fulfillment-parent") }, states: { inventory: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("fulfillment-inventory"), - logic: makeInvokeLogic(inventoryStarted, inventoryStopping) - }), + invoke: { src: "source2", id: "request", address: Machine.childAddress("fulfillment-inventory") }, states: { checking: { on: { - ReserveInventory: (to) => - to.branch.success().resolve(({ target }) => target.decoded(new Success({ requestId: "done" }))) + ReserveInventory: { + target: targets87.root.success, + decoded: () => (new Success({ requestId: "done" })) + } } } } }, shipping: { - invoke: (from) => - from.logic("request", { - address: Machine.childAddress("fulfillment-shipping"), - logic: makeInvokeLogic(shippingStarted, shippingStopping) - }) + invoke: { src: "source3", id: "request", address: Machine.childAddress("fulfillment-shipping") } } } }, @@ -6840,9 +7010,11 @@ describe("Machine", () => { it.effect("fails when always transitions do not stabilize", () => Effect.gen(function*() { + const root88 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets88 = Machine.targets(root88) const machine = Machine.make({ id: "LoopMachine", - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root88, events: Machine.eventsFromSchemas(Submit), input: Input, initialConfiguration: (root) => @@ -6852,15 +7024,13 @@ describe("Machine", () => { }).handle({ states: { Idle: { - always: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))), + always: { target: targets88.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) }, on: { - Submit: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + Submit: { target: targets88.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } } }, Loading: { - always: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-1" }))) + always: { target: targets88.root.Idle, decoded: () => (new Idle({ userId: "user-1" })) } } } }) @@ -6881,20 +7051,21 @@ describe("Machine", () => { it.effect("fails initial planning and startup when always transitions do not stabilize", () => Effect.gen(function*() { + const root89 = Machine.state({ initial: "Idle", states: { Idle, Loading } }) + const targets89 = Machine.targets(root89) const machine = Machine.make({ id: "InitialLoopMachine", - root: Machine.state({ initial: "Idle", states: { Idle, Loading } }), + root: root89, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.Idle.decoded(new Idle({ userId: "user-1" })))) }).handle({ states: { Idle: { - always: (to) => - to.branch.Loading().resolve(({ target }) => target.decoded(new Loading({ requestId: "request-1" }))) + always: { target: targets89.root.Loading, decoded: () => (new Loading({ requestId: "request-1" })) } }, Loading: { - always: (to) => to.branch.Idle().resolve(({ target }) => target.decoded(new Idle({ userId: "user-1" }))) + always: { target: targets89.root.Idle, decoded: () => (new Idle({ userId: "user-1" })) } } } }) @@ -6928,7 +7099,13 @@ describe("Machine", () => { } } }) + const targets90 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets90.root.flow } }, + transition2: { destination: { target: targets90.root.flow } } + }, + id: "CompletionLoopMachine", root: states, events: Machine.eventsFromSchemas(Submit), @@ -6938,23 +7115,25 @@ describe("Machine", () => { states: { idle: { on: { - Submit: (to) => - to.branch.flow().resolve(({ target }) => + Submit: { + branches: "transition1", + resolve: ({ select: { destination: target } }) => target.decoded( new Loading({ requestId: "request-1" }), (flow) => flow.done.decoded(new Success({ requestId: "request-1" })) ) - ) + } } }, flow: { - onDone: (to) => - to.branch.flow().resolve(({ state, target }) => + onDone: { + branches: "transition2", + resolve: ({ state, select: { destination: target } }) => target.decoded( state, (flow) => flow.done.decoded(new Success({ requestId: state.requestId })) ) - ) + } } } }) @@ -7006,15 +7185,19 @@ describe("Machine", () => { } }) - const makeParallelCounterMachine = () => - Machine.make({ + const makeParallelCounterMachine = () => { + const targets91 = Machine.targets(ParallelCounterStates) + return Machine.make({ root: ParallelCounterStates, events: Machine.eventsFromSchemas(AdvanceCounters), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => - to.running.decoded(new CounterRunning({}), (running) => - running.left.decoded(new LeftCounter({ value: 0 })).right.decoded(new RightCounter({ value: 0 }))) + to.running.decoded( + new CounterRunning({}), + (running) => + running.left.decoded(new LeftCounter({ value: 0 })).right.decoded(new RightCounter({ value: 0 })) + ) ) ) }).handle({ @@ -7023,24 +7206,25 @@ describe("Machine", () => { states: { left: { on: { - AdvanceCounters: (to) => - to.branch.running.left().resolve(({ state, target }) => - target.decoded(new LeftCounter({ value: state.value + 1 })) - ) + AdvanceCounters: { + target: targets91.root.running.left, + decoded: ({ state }) => (new LeftCounter({ value: state.value + 1 })) + } } }, right: { on: { - AdvanceCounters: (to) => - to.branch.running.right().resolve(({ state, target }) => - target.decoded(new RightCounter({ value: state.value + 1 })) - ) + AdvanceCounters: { + target: targets91.root.running.right, + decoded: ({ state }) => (new RightCounter({ value: state.value + 1 })) + } } } } } } }) + } const makeConcurrentMachine = () => { const states = Machine.state({ initial: "ConcurrentIdle", states: { ConcurrentIdle } }) @@ -7053,7 +7237,7 @@ describe("Machine", () => { states: { ConcurrentIdle: { on: { - ConcurrentPing: (to) => to.none + ConcurrentPing: { none: true } } } } diff --git a/packages/effect-machine/test/machine/MachineReferences.test.ts b/packages/effect-machine/test/machine/MachineReferences.test.ts index 92f2d05d..188fd86b 100644 --- a/packages/effect-machine/test/machine/MachineReferences.test.ts +++ b/packages/effect-machine/test/machine/MachineReferences.test.ts @@ -131,11 +131,13 @@ describe("machine reference event channels", () => { states: { Idle: { on: { - Publish: (to) => - to.none.resolve(({ event }, enqueue) => { + Publish: { + none: true, + resolve: ({ event }, enqueue) => { enqueue.emit(Emissions.Published({ value: event.value })) return undefined - }) + } + } } } } @@ -184,11 +186,13 @@ describe("machine reference event channels", () => { states: { Idle: { on: { - Publish: (to) => - to.none.resolve((_, enqueue) => { + Publish: { + none: true, + resolve: (_, enqueue) => { enqueue.emit(Emissions.Published({ value: "invalid" } as never)) return undefined - }) + } + } } } } @@ -230,7 +234,10 @@ describe("machine reference event channels", () => { const ChildEmissions = Machine.emittedEventsFromSchemas(Notice) const childStates = Machine.state({ initial: "Waiting", states: { Waiting, Reported } }) let rootHadParent = true + const targets3 = Machine.targets(childStates) const childMachine = Machine.make({ + branches: { transition1: { destination: { target: targets3.root.Reported } } }, + root: childStates, events: ChildEvents, parent: Machine.optionalParent(ParentEvents), @@ -241,15 +248,17 @@ describe("machine reference event channels", () => { states: { Waiting: { on: { - Trigger: (to) => - to.branch.Reported().resolve(({ parent, target }, enqueue) => { + Trigger: { + branches: "transition1", + resolve: ({ parent, select: { destination: target } }, enqueue) => { rootHadParent = parent !== undefined enqueue.emit(ChildEmissions.Notice({ value: 1 })) if (parent !== undefined) { enqueue.sendTo(parent, ParentEvents.ChildReported({ value: 1 })) } return target.decoded(new Reported({})) - }) + } + } } }, Reported: {} @@ -277,7 +286,11 @@ describe("machine reference event channels", () => { Finished: { schema: Finished, type: "final", output: Schema.String } } }) + const targets4 = Machine.targets(parentStates) const parentMachine = Machine.make({ + branches: { transition1: { destination: { target: targets4.root.Finished } } }, + children: { source1: Child }, + root: parentStates, events: Machine.eventsFromSchemas(ParentEvents, Notice), initialConfiguration: (root) => @@ -285,12 +298,13 @@ describe("machine reference event channels", () => { }).handle({ states: { Awaiting: { - invoke: (from) => from.child(Child), + invoke: { src: "source1" }, on: { - ChildReported: (to) => - to.branch.Finished().resolve(({ target }) => target.decoded(new Finished({ source: "parent event" }))), - Notice: (to) => - to.branch.Finished().resolve(({ target }) => target.decoded(new Finished({ source: "emission" }))) + ChildReported: { + target: targets4.root.Finished, + decoded: () => (new Finished({ source: "parent event" })) + }, + Notice: { target: targets4.root.Finished, decoded: () => (new Finished({ source: "emission" })) } } }, Finished: { output: ({ state }) => state.source } @@ -320,6 +334,23 @@ describe("machine reference event channels", () => { const ParentEvents = Machine.eventsFromSchemas(ChildReady) const childStates = Machine.state({ initial: "ChildIdle", states: { ChildIdle } }) const childDefinition = Machine.make({ + effects: { + source1: ({ + parent + }: Machine.Machine.InvokeContext< + { + readonly "": + & { readonly initial: "ChildIdle"; readonly states: { readonly ChildIdle: typeof ChildIdle } } + & { readonly "~effect/Machine/ExplicitInitial": true } + }, + readonly [], + readonly [], + "ChildIdle", + readonly [], + Machine.Machine.ParentEventSchemas<"required", readonly [typeof ChildReady]> + >) => parent.send(ParentEvents.ChildReady()) + }, + root: childStates, events: Machine.eventsFromSchemas(), parent: Machine.parent(ParentEvents), @@ -329,11 +360,13 @@ describe("machine reference event channels", () => { const childMachine = childDefinition.handle({ states: { ChildIdle: { - invoke: (from) => - from.effect("notify-ready", ({ parent }) => parent.send(ParentEvents.ChildReady())).onDone((to) => - to.none - ) - .onFailure((to) => to.none) + invoke: { + src: "source1", + id: "notify-ready", + input: (context) => context, + onDone: { none: true }, + onFailure: { none: true } + } } } }) @@ -346,7 +379,10 @@ describe("machine reference event channels", () => { ParentDone: { schema: ParentDone, type: "final", output: Schema.Void } } }) + const targets6 = Machine.targets(parentStates) const parentMachine = Machine.make({ + children: { source1: Child }, + root: parentStates, events: ParentEvents, initialConfiguration: (root) => @@ -354,9 +390,9 @@ describe("machine reference event channels", () => { }).handle({ states: { ParentWaiting: { - invoke: (from) => from.child(Child).onFailure((to) => to.none), + invoke: { src: "source1", onFailure: { none: true } }, on: { - ChildReady: (to) => to.branch.ParentDone().resolve(({ target }) => target.decoded(new ParentDone({}))) + ChildReady: { target: targets6.root.ParentDone, decoded: () => (new ParentDone({})) } } }, ParentDone: { output: () => undefined } diff --git a/packages/effect-machine/test/machine/ProtocolOwnership.test.ts b/packages/effect-machine/test/machine/ProtocolOwnership.test.ts index ffcd039d..db3e0d08 100644 --- a/packages/effect-machine/test/machine/ProtocolOwnership.test.ts +++ b/packages/effect-machine/test/machine/ProtocolOwnership.test.ts @@ -9,8 +9,9 @@ describe("protocol ownership", () => { const Set = Schema.TaggedStruct("Set", { value: Schema.Int }) const events = Machine.eventsFromSchemas(Set) let retained: unknown + const root1 = Machine.state({ initial: "State", states: { State } }) const machine = Machine.make({ - root: Machine.state({ initial: "State", states: { State } }), + root: root1, events, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.State.decoded({ _tag: "State" }))) @@ -18,10 +19,12 @@ describe("protocol ownership", () => { states: { State: { on: { - Set: (to) => - to.none.resolve(({ event }) => { + Set: { + none: true, + resolve: ({ event }) => { retained = event - }) + } + } } } } @@ -40,14 +43,15 @@ describe("protocol ownership", () => { const State = Schema.TaggedStruct("State", {}) const Internal = Schema.TaggedStruct("Internal", {}) const internalEvents = Machine.internalEventsFromSchemas(Internal) + const root2 = Machine.state({ initial: "State", states: { State } }) const machine = Machine.make({ - root: Machine.state({ initial: "State", states: { State } }), + root: root2, events: Machine.eventsFromSchemas(), internalEvents, initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => to.State.decoded({ _tag: "State" }))) }) - .handle({ states: { State: { on: { Internal: (to) => to.none } } } }) + .handle({ states: { State: { on: { Internal: { none: true } } } } }) const initial = yield* Machine.planInitial(machine) assert.isTrue(yield* Machine.can(machine, initial.state, { _tag: "Internal" })) assert.isTrue(yield* Machine.can(machine)(initial.state, internalEvents.Internal())) diff --git a/packages/effect-machine/test/machine/Resume.test.ts b/packages/effect-machine/test/machine/Resume.test.ts index a308f026..5270fafe 100644 --- a/packages/effect-machine/test/machine/Resume.test.ts +++ b/packages/effect-machine/test/machine/Resume.test.ts @@ -71,6 +71,15 @@ describe("Machine.resume", () => { } }) const machine = Machine.make({ + logic: { + source1: restoredLogic("root"), + source2: restoredLogic("left"), + source3: restoredLogic("left-leaf"), + source4: restoredLogic("right"), + source5: restoredLogic("right-leaf"), + source6: restoredLogic("inactive") + }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -78,40 +87,28 @@ describe("Machine.resume", () => { }).handle({ states: { Root: { - invoke: (from) => - from.logic("root", { address: Machine.childAddress("root"), logic: restoredLogic("root") }), + invoke: { src: "source1", id: "root", address: Machine.childAddress("root") }, states: { left: { - invoke: (from) => - from.logic("left", { address: Machine.childAddress("left"), logic: restoredLogic("left") }), + invoke: { src: "source2", id: "left", address: Machine.childAddress("left") }, states: { On: { - invoke: (from) => - from.logic("left-leaf", { - address: Machine.childAddress("left-leaf"), - logic: restoredLogic("left-leaf") - }) + invoke: { src: "source3", id: "left-leaf", address: Machine.childAddress("left-leaf") } } } }, right: { - invoke: (from) => - from.logic("right", { address: Machine.childAddress("right"), logic: restoredLogic("right") }), + invoke: { src: "source4", id: "right", address: Machine.childAddress("right") }, states: { On: { - invoke: (from) => - from.logic("right-leaf", { - address: Machine.childAddress("right-leaf"), - logic: restoredLogic("right-leaf") - }) + invoke: { src: "source5", id: "right-leaf", address: Machine.childAddress("right-leaf") } } } } } }, Inactive: { - invoke: (from) => - from.logic("inactive", { address: Machine.childAddress("inactive"), logic: restoredLogic("inactive") }) + invoke: { src: "source6", id: "inactive", address: Machine.childAddress("inactive") } } } }) @@ -186,6 +183,7 @@ describe("Machine.resume", () => { } } } + const targets2 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Advance), @@ -199,7 +197,7 @@ describe("Machine.resume", () => { states: { A: { on: { - Advance: (to) => to.local.B().resolve(({ target }) => target.decoded(new LeftB({}))) + Advance: { target: targets2.root.Root.left.B, decoded: () => (new LeftB({})) } } } } @@ -208,7 +206,7 @@ describe("Machine.resume", () => { states: { A: { on: { - Advance: (to) => to.local.B().resolve(({ target }) => target.decoded(new RightB({}))) + Advance: { target: targets2.root.Root.right.B, decoded: () => (new RightB({})) } } } } @@ -254,6 +252,7 @@ describe("Machine.resume", () => { Done: { schema: Done, type: "final", output: Schema.Number } } }) + const targets3 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Finish), @@ -263,7 +262,7 @@ describe("Machine.resume", () => { states: { Count: { on: { - Finish: (to) => to.branch.Done().resolve(({ target }) => target.decoded(new Done({ value: 9 }))) + Finish: { target: targets3.root.Done, decoded: () => (new Done({ value: 9 })) } } }, Done: { output: ({ state }) => state.value } @@ -310,6 +309,7 @@ describe("Machine.resume", () => { { path: "Flow" as const, output: undefined } ] } + const targets4 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Ping), @@ -318,7 +318,7 @@ describe("Machine.resume", () => { .handle({ states: { Flow: { - onDone: (to) => to.branch.Next().resolve(({ target }) => target.decoded(new Next({}))) + onDone: { target: targets4.root.Next, decoded: () => (new Next({})) } }, Next: {} } @@ -338,6 +338,7 @@ describe("Machine.resume", () => { class A extends Schema.TaggedClass("A")("A", {}) {} class B extends Schema.TaggedClass("B")("B", {}) {} const states = Machine.state({ initial: "A", states: { A, B } }) + const targets5 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Ping), @@ -345,7 +346,7 @@ describe("Machine.resume", () => { }).handle({ states: { A: { - always: (to) => to.branch.B().resolve(({ target }) => target.decoded(new B({}))) + always: { target: targets5.root.B, decoded: () => (new B({})) } }, B: {} } @@ -369,7 +370,10 @@ describe("Machine.resume", () => { class Cancelled extends Schema.TaggedClass("Cancelled")("Cancelled", {}) {} class TimedOut extends Schema.TaggedClass("TimedOut")("TimedOut", {}) {} const states = Machine.state({ initial: "Waiting", states: { Waiting, Cancelled, TimedOut } }) + const targets6 = Machine.targets(states) const machine = Machine.make({ + timers: { source1: "1 second" }, + root: states, events: Machine.eventsFromSchemas(Cancel), internalEvents: Machine.internalEventsFromSchemas(Timeout), @@ -378,12 +382,13 @@ describe("Machine.resume", () => { }).handle({ states: { Waiting: { - invoke: (from) => - from.timer("timeout", "1 second").onDone((to) => - to.branch.TimedOut().resolve(({ target }) => target.decoded(new TimedOut({}))) - ), + invoke: { + src: "source1", + id: "timeout", + onDone: { target: targets6.root.TimedOut, decoded: () => (new TimedOut({})) } + }, on: { - Cancel: (to) => to.branch.Cancelled().resolve(({ target }) => target.decoded(new Cancelled({}))) + Cancel: { target: targets6.root.Cancelled, decoded: () => (new Cancelled({})) } } }, Cancelled: {}, @@ -422,7 +427,10 @@ describe("Machine.resume", () => { }) {} const runs = yield* Ref.make(0) const states = Machine.state({ initial: "Loading", states: { Loading, Loaded } }) + const targets7 = Machine.targets(states) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh"))) }, + root: states, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(LoadedEvent), @@ -431,10 +439,11 @@ describe("Machine.resume", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => Ref.updateAndGet(runs, (n) => n + 1).pipe(Effect.as("fresh"))).onDone((to) => - to.branch.Loaded().resolve(({ output, target }) => target.decoded(new Loaded({ value: output }))) - ) + invoke: { + src: "source1", + id: "load", + onDone: { target: targets7.root.Loaded, decoded: ({ output }) => (new Loaded({ value: output })) } + } }, Loaded: {} } @@ -466,7 +475,16 @@ describe("Machine.resume", () => { }) {} const runs = yield* Ref.make(0) const states = Machine.state({ initial: "Loading", states: { Loading, Failed } }) + const targets8 = Machine.targets(states) const machine = Machine.make({ + effects: { + source1: Effect.suspend(() => + Ref.update(runs, (n) => n + 1).pipe( + Effect.andThen(Effect.fail(new LoadFailure({ message: "offline" }))) + ) + ) + }, + root: states, events: Machine.eventsFromSchemas(), internalEvents: Machine.internalEventsFromSchemas(FailedEvent), @@ -475,15 +493,14 @@ describe("Machine.resume", () => { }).handle({ states: { Loading: { - invoke: (from) => - from.effect("load", () => - Ref.update(runs, (n) => n + 1).pipe( - Effect.andThen(Effect.fail(new LoadFailure({ message: "offline" }))) - )).onFailure((to) => - to.branch.Failed().resolve(({ error, target }) => - target.decoded(new Failed({ message: error.message })) - ) - ) + invoke: { + src: "source1", + id: "load", + onFailure: { + target: targets8.root.Failed, + decoded: ({ error }) => (new Failed({ message: error.message })) + } + } }, Failed: {} } @@ -519,6 +536,7 @@ describe("Machine.resume", () => { ChildDone: { schema: ChildDone, type: "final", output: Schema.Number } } }) + const targets9 = Machine.targets(childStates) const child = Machine.make({ root: childStates, events: Machine.eventsFromSchemas(ChildFinish), @@ -528,10 +546,10 @@ describe("Machine.resume", () => { states: { ChildIdle: { on: { - ChildFinish: (to) => - to.branch.ChildDone().resolve(({ state, target }) => - target.decoded(new ChildDone({ value: state.value + 1 })) - ) + ChildFinish: { + target: targets9.root.ChildDone, + decoded: ({ state }) => (new ChildDone({ value: state.value + 1 })) + } } }, ChildDone: { output: ({ state }) => state.value } @@ -539,7 +557,10 @@ describe("Machine.resume", () => { }) const Child = Machine.child("child", child) const states = Machine.state({ initial: "Parent", states: { Parent, ChildOutput } }) + const targets10 = Machine.targets(states) const machine = Machine.make({ + children: { source1: Child }, + root: states, events: Machine.eventsFromSchemas(ChildOutput), initialConfiguration: (root) => @@ -547,12 +568,13 @@ describe("Machine.resume", () => { }).handle({ states: { Parent: { - invoke: (from) => - from.child(Child).onDone((to) => - to.branch.ChildOutput().resolve(({ output, target }) => - target.decoded(new ChildOutput({ value: output })) - ) - ) + invoke: { + src: "source1", + onDone: { + target: targets10.root.ChildOutput, + decoded: ({ output }) => (new ChildOutput({ value: output })) + } + } }, ChildOutput: {} } @@ -665,6 +687,7 @@ describe("Machine.resume", () => { it.effect("obeys bounded encode/decode continuation equivalence", () => Effect.gen(function*() { const states = Machine.state({ initial: "Count", states: { Count } }) + const targets11 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Add), @@ -674,10 +697,10 @@ describe("Machine.resume", () => { states: { Count: { on: { - Add: (to) => - to.branch.Count().resolve(({ event, state, target }) => - target.decoded(new Count({ value: state.value + event.value })) - ) + Add: { + target: targets11.root.Count, + decoded: ({ event, state }) => (new Count({ value: state.value + event.value })) + } } } } diff --git a/packages/effect-machine/test/machine/Root.test.ts b/packages/effect-machine/test/machine/Root.test.ts index dbad228b..6481dec8 100644 --- a/packages/effect-machine/test/machine/Root.test.ts +++ b/packages/effect-machine/test/machine/Root.test.ts @@ -4,13 +4,14 @@ import { Machine } from "../../src/index.js" const CounterRoot = Machine.state({ fields: { count: Schema.Number } }) const Events = Machine.events({ Increment: { by: Schema.Number } }) +const targets1 = Machine.targets(CounterRoot) const counter = Machine.make({ root: CounterRoot, events: Events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ on: { - Increment: (to) => to.self.update.from(({ current, event }) => ({ count: current.count + event.by })) + Increment: { update: targets1.root, from: ({ root: current, event }) => ({ count: current.count + event.by }) } } }) @@ -31,15 +32,18 @@ const EditorRoot = Machine.state({ states: { Editing: {}, Saving: {} } }) const EditorEvents = Machine.events({ Save: {}, Edit: {}, Increment: { by: Schema.Number } }) +const targets2 = Machine.targets(EditorRoot) const editor = Machine.make({ root: EditorRoot, events: EditorEvents, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ - on: { Increment: (to) => to.self.update.from(({ current, event }) => ({ count: current.count + event.by })) }, + on: { + Increment: { update: targets2.root, from: ({ root: current, event }) => ({ count: current.count + event.by }) } + }, states: { - Editing: { on: { Save: (to) => to.local.Saving() } }, - Saving: { on: { Edit: (to) => to.local.Editing() } } + Editing: { on: { Save: { target: targets2.root.Saving } } }, + Saving: { on: { Edit: { target: targets2.root.Editing } } } } }) @@ -94,16 +98,25 @@ it("declines guarded child transitions and tries the root handler", async () => const root = Machine.state({ fields: { count: Schema.Number }, initial: "Idle", states: { Idle: {}, Busy: {} } }) let constructed = 0 const events = Machine.events({ Go: { allowed: Schema.Boolean } }) - const machine = Machine.make({ root, events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ - on: { Go: (to) => to.self.update.from(({ current }) => ({ count: current.count + 1 })) }, + const targets3 = Machine.targets(root) + const machine = Machine.make({ + branches: { transition1: { destination: { target: targets3.root.Busy } } }, + root, + events, + initial: (root) => root.from(() => ({ count: 0 })) + }).handle({ + on: { Go: { update: targets3.root, from: ({ root: current }) => ({ count: current.count + 1 }) } }, states: { Idle: { on: { - Go: (to) => - to.local.Busy().guard(({ event }) => event.allowed).resolve(({ target }) => { + Go: { + branches: "transition1", + guard: ({ event }) => event.allowed, + resolve: ({ select: { destination: target } }) => { constructed++ return target.from() - }) + } + } } } } @@ -146,6 +159,7 @@ it("returns the completed workflow output through the root boundary", async () = Workflow: { initial: "Working", states: { Working: {}, Finished: { type: "final", output: Schema.Number } } } } }) + const targets4 = Machine.targets(root) const machine = Machine.make({ root, events: Machine.events({ Finish: {} }), @@ -154,7 +168,7 @@ it("returns the completed workflow output through the root boundary", async () = states: { Workflow: { states: { - Working: { on: { Finish: (to) => to.local.Finished() } }, + Working: { on: { Finish: { target: targets4.root.Workflow.Finished } } }, Finished: { output: () => 42 } } } @@ -193,19 +207,25 @@ it("retains current root fields when restoring descendant history", async () => } }) const events = Machine.events({ Next: {}, Leave: {}, Return: {}, Increment: {} }) - const machine = Machine.make({ root, events, initial: (root) => root.from(() => ({ count: 0 })) }).handle({ - on: { Increment: (to) => to.self.update.from(({ current }) => ({ count: current.count + 1 })) }, + const targets5 = Machine.targets(root) + const machine = Machine.make({ + branches: { transition1: { destination: { history: targets5.root.Editing.recent } } }, + root, + events, + initial: (root) => root.from(() => ({ count: 0 })) + }).handle({ + on: { Increment: { update: targets5.root, from: ({ root: current }) => ({ count: current.count + 1 }) } }, states: { Editing: { - on: { Leave: (to) => to.branch.Away() }, + on: { Leave: { target: targets5.root.Away } }, history: { recent: { default: ({ target }) => target.from({ count: 999 }, (to) => to.Editing.from((editing) => editing.A.from())) } }, - states: { A: { on: { Next: (to) => to.local.B() } } } + states: { A: { on: { Next: { target: targets5.root.Editing.B } } } } }, - Away: { on: { Return: (to) => to.history.Editing.recent.resolve(({ target }) => target()) } } + Away: { on: { Return: { branches: "transition1", resolve: ({ select: { destination: target } }) => target() } } } } }) let snapshot = (await Effect.runPromise(Machine.planInitial(machine))).state @@ -218,8 +238,9 @@ it("retains current root fields when restoring descendant history", async () => it("checks a bare guard before selecting a default destination", async () => { const root = Machine.state({ initial: "Idle", states: { Idle: {}, Busy: {} } }) + const targets6 = Machine.targets(root) const machine = Machine.make({ root, events: Machine.events({ Start: { allowed: Schema.Boolean } }) }).handle({ - states: { Idle: { on: { Start: (to) => to.local.Busy().guard(({ event }) => event.allowed) } } } + states: { Idle: { on: { Start: { target: targets6.root.Busy, guard: ({ event }) => event.allowed } } } } }) const initial = await Effect.runPromise(Machine.planInitial(machine)) const blocked = await Effect.runPromise(Machine.plan(machine, initial.state, { _tag: "Start", allowed: false })) diff --git a/packages/effect-machine/test/machine/RuntimeDifferential.test.ts b/packages/effect-machine/test/machine/RuntimeDifferential.test.ts index 7cfb0443..28b4ed20 100644 --- a/packages/effect-machine/test/machine/RuntimeDifferential.test.ts +++ b/packages/effect-machine/test/machine/RuntimeDifferential.test.ts @@ -53,6 +53,7 @@ describe("pure planning and managed runtime differential", () => { Done: { schema: Done, type: "final", output: Schema.Number } } }) + const targets1 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Cascade, Ignore, Finish), @@ -63,15 +64,18 @@ describe("pure planning and managed runtime differential", () => { states: { Count: { on: { - Cascade: (to) => - to.none.resolve((_, enqueue) => { + Cascade: { + none: true, + resolve: (_, enqueue) => { enqueue.raise(new Increment({})) return undefined - }), - Increment: (to) => - to.branch.Count().resolve(({ state, target }) => target.decoded(new Count({ value: state.value + 1 }))), - Finish: (to) => - to.branch.Done().resolve(({ state, target }) => target.decoded(new Done({ value: state.value }))) + } + }, + Increment: { + target: targets1.root.Count, + decoded: ({ state }) => (new Count({ value: state.value + 1 })) + }, + Finish: { target: targets1.root.Done, decoded: ({ state }) => (new Done({ value: state.value })) } } }, Done: { output: ({ state }) => state.value } @@ -148,7 +152,13 @@ describe("pure planning and managed runtime differential", () => { readonly left: number readonly right: number }> = [] + const targets2 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets2.root.Done } }, + transition2: { destination: { target: targets2.root.Running.Left } } + }, + root: states, events: Machine.eventsFromSchemas(Advance, Inspect, Finish), internalEvents: Machine.internalEventsFromSchemas(Bump), @@ -165,38 +175,43 @@ describe("pure planning and managed runtime differential", () => { states: { Running: { on: { - Finish: (to) => - to.branch.Done().resolve(({ snapshot, target }) => { + Finish: { + branches: "transition1", + resolve: ({ snapshot, select: { destination: target } }) => { if (snapshot.state.path !== "Running") throw new Error("expected Running snapshot") return target.decoded( new Done({ value: snapshot.state.states.Left.value.value + snapshot.state.states.Right.value.value }) ) - }) + } + } }, states: { Left: { on: { - Advance: (to) => - to.branch.Running.Left().resolve(({ state, target }, enqueue) => { + Advance: { + branches: "transition2", + resolve: ({ state, select: { destination: target } }, enqueue) => { enqueue.raise(new Bump({})) return target.decoded(new Left({ value: state.value + 1 })) - }) + } + } } }, Right: { on: { - Advance: (to) => - to.branch.Running.Right().resolve(({ state, target }) => - target.decoded(new Right({ value: state.value + 10 })) - ), - Bump: (to) => - to.branch.Running.Right().resolve(({ state, target }) => - target.decoded(new Right({ value: state.value + 100 })) - ), - Inspect: (to) => - to.none.resolve((context) => { + Advance: { + target: targets2.root.Running.Right, + decoded: ({ state }) => (new Right({ value: state.value + 10 })) + }, + Bump: { + target: targets2.root.Running.Right, + decoded: ({ state }) => (new Right({ value: state.value + 100 })) + }, + Inspect: { + none: true, + resolve: (context) => { const { state, containingState, ancestors, snapshot } = context if (snapshot.state.path !== "Running") throw new Error("expected Running snapshot") const expectedKeys = [ @@ -205,7 +220,9 @@ describe("pure planning and managed runtime differential", () => { "containingState", "ancestors", "event", - "snapshot" + "snapshot", + "root", + "decline" ] const spread = { ...context } assert.deepStrictEqual(Object.keys(context), expectedKeys) @@ -224,7 +241,8 @@ describe("pure planning and managed runtime differential", () => { right: snapshot.state.states.Right.value.value }) return undefined - }) + } + } } } } @@ -460,7 +478,13 @@ describe("pure planning and managed runtime differential", () => { Finished: { schema: Finished, type: "final", output: Schema.String } } }) + const targets3 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets3.root.Working } }, + transition2: { destination: { target: targets3.root.Finished } } + }, + root: states, events: Machine.eventsFromSchemas(Begin), internalEvents: Machine.internalEventsFromSchemas(RaisedOne, RaisedTwo), @@ -474,13 +498,15 @@ describe("pure planning and managed runtime differential", () => { enqueue.emit(new Notice({ label: "initial" })) }, on: { - Begin: (to) => - to.branch.Working().resolve(({ target }, enqueue) => { + Begin: { + branches: "transition1", + resolve: ({ select: { destination: target } }, enqueue) => { record("transition:begin") enqueue.emit(new Notice({ label: "transition" })) enqueue.raise(new RaisedOne({})) return target.decoded(new Working({})) - }) + } + } } }, Working: { @@ -490,18 +516,22 @@ describe("pure planning and managed runtime differential", () => { enqueue.raise(new RaisedTwo({})) }, on: { - RaisedOne: (to) => - to.none.resolve((_, enqueue) => { + RaisedOne: { + none: true, + resolve: (_, enqueue) => { record("raised:one") enqueue.emit(new Notice({ label: "raised-one" })) return undefined - }), - RaisedTwo: (to) => - to.branch.Finished().resolve(({ target }, enqueue) => { + } + }, + RaisedTwo: { + branches: "transition2", + resolve: ({ select: { destination: target } }, enqueue) => { record("raised:two") enqueue.emit(new Notice({ label: "raised-two" })) return target.decoded(new Finished({})) - }) + } + } } }, Finished: { @@ -577,6 +607,7 @@ describe("pure planning and managed runtime differential", () => { class Ignore extends Schema.TaggedClass("DifferentialIgnore")("Ignore", {}) {} class Go extends Schema.TaggedClass("DifferentialGo")("Go", {}) {} const states = Machine.state({ initial: "Idle", states: { Idle, Active } }) + const targets4 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Ignore, Go), @@ -585,7 +616,7 @@ describe("pure planning and managed runtime differential", () => { states: { Idle: { on: { - Go: (to) => to.branch.Active().resolve(({ target }) => target.decoded(new Active({}))) + Go: { target: targets4.root.Active, decoded: () => (new Active({})) } } }, Active: {} diff --git a/packages/effect-machine/test/machine/Scheduling.test.ts b/packages/effect-machine/test/machine/Scheduling.test.ts index 5d90faf6..e012e95f 100644 --- a/packages/effect-machine/test/machine/Scheduling.test.ts +++ b/packages/effect-machine/test/machine/Scheduling.test.ts @@ -17,7 +17,13 @@ describe("machine scheduling", () => { it.effect("drains a large synchronous raised-event burst without growing the stack", () => Effect.gen(function*() { const states = Machine.state({ initial: "SchedulingActive", states: { SchedulingActive } }) + const targets1 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets1.root.SchedulingActive } }, + transition2: { destination: { target: targets1.root.SchedulingActive } } + }, + root: states, events: Machine.eventsFromSchemas(StartBurst), internalEvents: Machine.internalEventsFromSchemas(Burst), @@ -29,17 +35,21 @@ describe("machine scheduling", () => { states: { SchedulingActive: { on: { - StartBurst: (to) => - to.branch.SchedulingActive().resolve(({ state, target }, enqueue) => { + StartBurst: { + branches: "transition1", + resolve: ({ state, select: { destination: target } }, enqueue) => { enqueue.raise(new Burst({})) return target.decoded(state) - }), - Burst: (to) => - to.branch.SchedulingActive().resolve(({ state, target }, enqueue) => { + } + }, + Burst: { + branches: "transition2", + resolve: ({ state, select: { destination: target } }, enqueue) => { const count = state.count + 1 if (count < burstSize) enqueue.raise(new Burst({})) return target.decoded(new SchedulingActive({ count })) - }) + } + } } } } diff --git a/packages/effect-machine/test/machine/SnapshotCodecAdversarial.test.ts b/packages/effect-machine/test/machine/SnapshotCodecAdversarial.test.ts index b29a295a..7721986c 100644 --- a/packages/effect-machine/test/machine/SnapshotCodecAdversarial.test.ts +++ b/packages/effect-machine/test/machine/SnapshotCodecAdversarial.test.ts @@ -411,6 +411,7 @@ describe("snapshot codec adversarial boundaries", () => { class Boundary extends Schema.TaggedClass("CodecAutomaticBoundary")("CodecAutomaticBoundary", {}) {} class After extends Schema.TaggedClass("CodecAutomaticAfter")("CodecAutomaticAfter", {}) {} const states = Machine.state({ initial: "Before", states: { Before, Boundary, After } }) + const targets1 = Machine.targets(states) const original = Machine.make({ id: "codec-automatic-original", root: states, @@ -420,12 +421,13 @@ describe("snapshot codec adversarial boundaries", () => { }).handle({ states: { Before: { - always: (to) => to.branch.Boundary().resolve(({ target }) => target.decoded(new Boundary({}))) + always: { target: targets1.root.Boundary, decoded: () => (new Boundary({})) } }, Boundary: {}, After: {} } }) + const targets2 = Machine.targets(states) const changed = Machine.make({ id: "codec-automatic-changed", root: states, @@ -436,7 +438,7 @@ describe("snapshot codec adversarial boundaries", () => { states: { Before: {}, Boundary: { - always: (to) => to.branch.After().resolve(({ target }) => target.decoded(new After({}))) + always: { target: targets2.root.After, decoded: () => (new After({})) } }, After: {} } diff --git a/packages/effect-machine/test/machine/SnapshotContext.test.ts b/packages/effect-machine/test/machine/SnapshotContext.test.ts index 8d5959bc..8ea139be 100644 --- a/packages/effect-machine/test/machine/SnapshotContext.test.ts +++ b/packages/effect-machine/test/machine/SnapshotContext.test.ts @@ -62,7 +62,15 @@ describe("Machine transition snapshot context", () => { it.effect("lets an effectful event handler inspect a sibling region", () => Effect.gen(function*() { let captured: Machine.Snapshot | undefined + const targets1 = Machine.targets(States) const machine = Machine.make({ + branches: { + transition1: { + online: { target: targets1.root.System.Playback.Playing, title: "Network is online" }, + unchanged: { none: true } + } + }, + root: States, events: Machine.eventsFromSchemas(BufferReady), initialConfiguration: initialDefinition @@ -74,16 +82,15 @@ describe("Machine transition snapshot context", () => { states: { Buffering: { on: { - BufferReady: (to) => - to.branches({ - online: { title: "Network is online", target: to.local.Playing() }, - unchanged: { target: to.none } - }).resolve(({ snapshot, select }) => { + BufferReady: { + branches: "transition1", + resolve: ({ snapshot, select }) => { captured = snapshot return States.matches(snapshot, "System.Network.Online") ? select.online.decoded(new Playing({})) : select.unchanged() - }) + } + } } } } @@ -107,7 +114,13 @@ describe("Machine transition snapshot context", () => { it.effect("shares one beginning-of-microstep snapshot across parallel transitions", () => Effect.gen(function*() { const captured: Array> = [] + const targets2 = Machine.targets(States) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets2.root.System.Playback.Playing } }, + transition2: { destination: { target: targets2.root.System.Network.Offline } } + }, + root: States, events: Machine.eventsFromSchemas(Disconnect), initialConfiguration: initialDefinition @@ -119,11 +132,13 @@ describe("Machine transition snapshot context", () => { states: { Buffering: { on: { - Disconnect: (to) => - to.local.Playing().resolve(({ snapshot, target }) => { + Disconnect: { + branches: "transition1", + resolve: ({ snapshot, select: { destination: target } }) => { captured.push(snapshot) return target.decoded(new Playing({})) - }) + } + } } } } @@ -132,11 +147,13 @@ describe("Machine transition snapshot context", () => { states: { Online: { on: { - Disconnect: (to) => - to.local.Offline().resolve(({ snapshot, target }) => { + Disconnect: { + branches: "transition2", + resolve: ({ snapshot, select: { destination: target } }) => { captured.push(snapshot) return target.decoded(new Offline({})) - }) + } + } } } } @@ -160,7 +177,15 @@ describe("Machine transition snapshot context", () => { it.effect("captures the complete configuration for an eventless transition", () => Effect.gen(function*() { let captured: Machine.Snapshot | undefined + const targets3 = Machine.targets(States) const machine = Machine.make({ + branches: { + transition1: { + online: { target: targets3.root.System.Playback.Playing, title: "Network is online" }, + unchanged: { none: true } + } + }, + root: States, events: Machine.eventsFromSchemas(), initialConfiguration: initialDefinition @@ -171,16 +196,15 @@ describe("Machine transition snapshot context", () => { Playback: { states: { Buffering: { - always: (to) => - to.branches({ - online: { title: "Network is online", target: to.local.Playing() }, - unchanged: { target: to.none } - }).resolve(({ snapshot, select }) => { + always: { + branches: "transition1", + resolve: ({ snapshot, select }) => { captured = snapshot return States.matches(snapshot, "System.Network.Online") ? select.online.decoded(new Playing({})) : select.unchanged() - }) + } + } } } } @@ -228,7 +252,10 @@ describe("Machine transition snapshot context", () => { } }) let captured: Machine.Snapshot | undefined + const targets4 = Machine.targets(completionStates) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets4.root.System.Work.Restarted } } }, + root: completionStates, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -245,11 +272,13 @@ describe("Machine transition snapshot context", () => { System: { states: { Work: { - onDone: (to) => - to.local.Restarted().resolve(({ snapshot, target }) => { + onDone: { + branches: "transition1", + resolve: ({ snapshot, select: { destination: target } }) => { captured = snapshot return target.decoded(new Restarted({})) - }) + } + } } } } diff --git a/packages/effect-machine/test/machine/StateUpdate.test.ts b/packages/effect-machine/test/machine/StateUpdate.test.ts index 171613bc..56718c26 100644 --- a/packages/effect-machine/test/machine/StateUpdate.test.ts +++ b/packages/effect-machine/test/machine/StateUpdate.test.ts @@ -26,7 +26,13 @@ describe("state value updates", () => { } }) let observedEntry: { readonly notice: string | null; readonly request: string } | undefined + const targets1 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { destination: { target: targets1.root.Ready.SavingPlan, update: targets1.root.Ready } }, + transition2: { destination: { target: targets1.root.Ready.SavingPlan, update: targets1.root.Ready } } + }, + root: states, events: Machine.eventsFromSchemas(Event), initialConfiguration: (root) => @@ -39,22 +45,18 @@ describe("state value updates", () => { states: { Idle: { on: { - CreatePlan: (to) => - to.local.SavingPlan() - .updating(to.branch.Ready) - .resolve(({ current, event, owner, target }) => - target.from({ request: event.input }).update( - owner.decoded(State.cases.Ready.make({ ...current, notice: null })) - ) - ), - InvalidPlan: (to) => - to.local.SavingPlan() - .updating(to.branch.Ready) - .resolve(({ owner, target }) => - target.from({ request: "invalid" }).update( - owner.from({ notice: 1 } as any) - ) + CreatePlan: { + branches: "transition1", + resolve: ({ ancestors: { "Ready": current }, event, select: { destination: target } }) => + target.from({ request: event.input }).update.decoded( + State.cases.Ready.make({ ...current, notice: null }) ) + }, + InvalidPlan: { + branches: "transition2", + resolve: ({ select: { destination: target } }) => + target.from({ request: "invalid" }).update.from({ notice: 1 } as any) + } } }, SavingPlan: { @@ -69,9 +71,11 @@ describe("state value updates", () => { }) assert.deepStrictEqual(Machine.transitionDefinitions(machine)[0]?.branches, [{ - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "Ready.SavingPlan", - selection: { kind: "state", scope: "local", path: "Ready.SavingPlan" }, + selection: { kind: "state", scope: "branch", path: "Ready.SavingPlan" }, updates: ["Ready"] }]) @@ -121,7 +125,11 @@ describe("state value updates", () => { } }) let idleSawDay: string | undefined + const targets2 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { target: targets2.root.Ready.Idle, update: targets2.root.Ready } } }, + effects: { source1: Effect.suspend(() => Effect.succeed("Monday")) }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -141,16 +149,15 @@ describe("state value updates", () => { } }, Saving: { - invoke: (from) => - from.effect("save", () => Effect.succeed("Monday")).onDone((to) => - to.local.Idle() - .updating(to.branch.Ready) - .resolve(({ current, output, owner, target }) => - target.from().update( - owner.decoded(State.cases.Ready.make({ ...current, day: output, notice: "Saved" })) - ) - ) - ) + invoke: { + src: "source1", + id: "save", + onDone: { + branches: "transition1", + resolve: ({ ancestors: { "Ready": current }, output, select: { destination: target } }) => + target.from().update.decoded(State.cases.Ready.make({ ...current, day: output, notice: "Saved" })) + } + } } } } @@ -193,14 +200,17 @@ describe("state value updates", () => { } } }) + const targets3 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), initialConfiguration: (root) => root.resolve(({ target }) => target.from((to) => - to.session.from({ count: 0 }, (session) => - session.editing.from({ draft: "kept" }, (editing) => editing.idle.from())) + to.session.from( + { count: 0 }, + (session) => session.editing.from({ draft: "kept" }, (editing) => editing.idle.from()) + ) ) ) }).handle({ @@ -211,10 +221,10 @@ describe("state value updates", () => { states: { idle: { on: { - Increment: (to) => - to.branch.session.update.resolve(({ current, owner }) => - owner.from({ count: current.count + 1 }) - ) + Increment: { + update: targets3.root.session, + from: ({ ancestors: { "session": current } }) => ({ count: current.count + 1 }) + } } } } @@ -284,7 +294,12 @@ describe("state value updates", () => { } } }) + const targets4 = Machine.targets(states) const machine = Machine.make({ + branches: { + transition1: { changed: { update: targets4.root.scope, title: "Value changed" }, unchanged: { none: true } } + }, + root: states, events: Machine.eventsFromSchemas(Event), initialConfiguration: (root) => @@ -295,13 +310,11 @@ describe("state value updates", () => { states: { idle: { on: { - Set: (to) => - to.branches({ - changed: { target: to.local.update, title: "Value changed" }, - unchanged: { target: to.none } - }).resolve(({ event, select }) => + Set: { + branches: "transition1", + resolve: ({ event, select }) => event.changed ? select.changed.from({ count: 1 }) : select.unchanged() - ) + } } } } @@ -314,7 +327,7 @@ describe("state value updates", () => { key: "changed", title: "Value changed", target: undefined, - selection: { kind: "update", scope: "local", path: "scope" }, + selection: { kind: "update", scope: "branch", path: "scope" }, updates: ["scope"] }, { type: "branch", @@ -350,6 +363,7 @@ describe("state value updates", () => { } } }) + const targets5 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), @@ -377,12 +391,15 @@ describe("state value updates", () => { return undefined }, on: { - Quiet: (to) => - to.local.update.resolve(({ current, owner }) => owner.from({ count: current.count + 1 })), - Loud: (to) => - to.local.update.reenter().resolve( - ({ current, owner }) => owner.from({ count: current.count + 1 }) - ) + Quiet: { + update: targets5.root.scope, + from: ({ ancestors: { "scope": current } }) => ({ count: current.count + 1 }) + }, + Loud: { + update: targets5.root.scope, + reenter: true, + from: ({ ancestors: { "scope": current } }) => ({ count: current.count + 1 }) + } } } } @@ -416,7 +433,10 @@ describe("state value updates", () => { } } }) + const targets6 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { update: targets6.root.scope } } }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -426,11 +446,14 @@ describe("state value updates", () => { scope: { states: { idle: { - always: (to) => - to.local.update.resolve(({ current, decline, owner }) => + always: { + branches: "transition1", + resolve: ({ ancestors: { "scope": current }, decline, select: { destination: owner } }) => current.count < 2 ? owner.from({ count: current.count + 1 }) - : decline(), { declinable: true }) + : decline(), + declinable: true + } } } } @@ -470,7 +493,10 @@ describe("state value updates", () => { state: { path: "root.a" as const, value: State.cases.A.make({}) } } }) + const targets7 = Machine.targets(states) const machine = Machine.make({ + branches: { transition3: { destination: { history: targets7.root.root.recent } } }, + root: states, events: Machine.eventsFromSchemas(Event), initialConfiguration: (to) => to.resolve(() => initialRoot()) @@ -481,16 +507,18 @@ describe("state value updates", () => { states: { a: { on: { - Leave: (to) => to.branch.outside().resolve(({ target }) => target.from()), - Update: (to) => - to.local.update.resolve(({ current, owner }) => owner.from({ count: current.count + 1 })) + Leave: { target: targets7.root.outside }, + Update: { + update: targets7.root.root, + from: ({ ancestors: { "root": current } }) => ({ count: current.count + 1 }) + } } } } }, outside: { on: { - Return: (to) => to.history.root.recent.resolve(({ target }) => target()) + Return: { branches: "transition3", resolve: ({ select: { destination: target } }) => target() } } } } @@ -542,6 +570,7 @@ describe("state value updates", () => { } }) let completions = 0 + const targets8 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Event), @@ -558,21 +587,23 @@ describe("state value updates", () => { root: { states: { left: { - onDone: (to) => - to.none.resolve(() => { + onDone: { + none: true, + resolve: () => { completions += 1 return undefined - }), + } + }, states: { done: { output: () => "complete" } } }, right: { states: { idle: { on: { - Update: (to) => - to.branch.root.update.resolve(({ current, owner }) => - owner.from({ revision: current.revision + 1 }) - ) + Update: { + update: targets8.root.root, + from: ({ ancestors: { "root": current } }) => ({ revision: current.revision + 1 }) + } } } } @@ -606,6 +637,7 @@ describe("state value updates", () => { } } }) + const targets9 = Machine.targets(states) const machine = Machine.make({ id: "state-update-schema", root: states, @@ -618,7 +650,7 @@ describe("state value updates", () => { states: { idle: { on: { - Break: (to) => to.local.update.resolve(({ owner }) => owner.from({ count: "bad" } as any)) + Break: { update: targets9.root.scope, from: () => ({ count: "bad" } as any) } } } } @@ -647,7 +679,10 @@ describe("state value updates", () => { } }) let starts = 0 + const targets10 = Machine.targets(states) const machine = Machine.make({ + effects: { source1: Effect.suspend(() => Effect.sync(() => ++starts)) }, + root: states, events: Machine.eventsFromSchemas(), initialConfiguration: (root) => @@ -657,10 +692,11 @@ describe("state value updates", () => { scope: { states: { idle: { - invoke: (from) => - from.effect("load", () => Effect.sync(() => ++starts)).onDone((to) => - to.local.update.resolve(({ output, owner }) => owner.from({ count: output })) - ) + invoke: { + src: "source1", + id: "load", + onDone: { update: targets10.root.scope, from: ({ output }) => ({ count: output }) } + } } } } @@ -692,7 +728,10 @@ describe("state value updates", () => { } } }) + const targets11 = Machine.targets(states) const machine = Machine.make({ + branches: { transition1: { destination: { update: targets11.root.scope } } }, + root: states, events: Events, emittedEvents: Emissions, @@ -704,14 +743,16 @@ describe("state value updates", () => { states: { idle: { on: { - Update: (to) => - to.local.update.resolve(({ self, owner }, enqueue) => { + Update: { + branches: "transition1", + resolve: ({ self, select: { destination: owner } }, enqueue) => { enqueue.raise(Events.Raised()) enqueue.emit(Emissions.Changed({ count: 1 })) enqueue.sendTo(self, Events.Raised()) return owner.from({ count: 1 }) - }), - Raised: (to) => to.none + } + }, + Raised: { none: true } } } } diff --git a/packages/effect-machine/test/machine/StructuralStates.test.ts b/packages/effect-machine/test/machine/StructuralStates.test.ts index 8b37e01b..56158410 100644 --- a/packages/effect-machine/test/machine/StructuralStates.test.ts +++ b/packages/effect-machine/test/machine/StructuralStates.test.ts @@ -72,8 +72,15 @@ const States = Machine.state({ } }) -const makeMachine = () => - Machine.make({ +const makeMachine = () => { + const targets1 = Machine.targets(States) + return Machine.make({ + branches: { + transition1: { destination: { target: targets1.root.player.transport.Loading } }, + transition2: { destination: { target: targets1.root.player.transport.Ready } }, + transition3: { destination: { target: targets1.root.player.transport.Ready.Playing } } + }, + root: States, events: Machine.eventsFromSchemas(SourceSelected, Loaded, Play, Mute), initialConfiguration: (root) => @@ -94,42 +101,48 @@ const makeMachine = () => states: { Empty: { on: { - SourceSelected: (to) => - to.local.Loading().resolve(({ event, state, target }) => { + SourceSelected: { + branches: "transition1", + resolve: ({ event, state, select: { destination: target } }) => { assert.strictEqual(state, undefined) return target.from({ url: event.url }) - }) + } + } } }, Loading: { on: { - Loaded: (to) => - to.local.Ready().resolve(({ event, state, target }) => { + Loaded: { + branches: "transition2", + resolve: ({ event, state, select: { destination: target } }) => { assert.strictEqual(state._tag, "Loading") return target.from( { duration: event.duration }, (ready) => ready.Paused.from() ) - }) + } + } } }, Ready: { states: { Paused: { on: { - Play: (to) => - to.local.Playing().resolve(({ containingState, state, target }) => { + Play: { + branches: "transition3", + resolve: ({ containingState, state, select: { destination: target } }) => { assert.strictEqual(state, undefined) return target.from({ position: Math.min(0, containingState.duration) }) - }) + } + } } }, Playing: { on: { - Mute: (to) => - to.branch.player.settings.Muted().resolve(({ event, target }) => - target.from({ volume: event.volume }) - ) + Mute: { + target: targets1.root.player.settings.Muted, + from: ({ event }) => ({ volume: event.volume }) + } } } } @@ -140,6 +153,7 @@ const makeMachine = () => } } }) +} const HistoryStates = Machine.state({ initial: "away", @@ -176,7 +190,15 @@ const historyFallback = () => ({ } }) +const targets2 = Machine.targets(HistoryStates) const historyMachine = Machine.make({ + branches: { + transition1: { destination: { target: targets2.root.away } }, + transition2: { destination: { target: targets2.root.flow.section.Editing } }, + transition3: { destination: { history: targets2.root.flow.recent } }, + transition4: { destination: { history: targets2.root.flow.exact } } + }, + root: HistoryStates, events: Machine.eventsFromSchemas(Edit, Leave, ResumeShallow, ResumeDeep), initialConfiguration: (root) => @@ -191,14 +213,14 @@ const historyMachine = Machine.make({ exact: { default: historyFallback } }, on: { - Leave: (to) => to.branch.away().resolve(({ target }) => target.from()) + Leave: { target: targets2.root.away } }, states: { section: { states: { Idle: { on: { - Edit: (to) => to.local.Editing().resolve(({ event, target }) => target.from({ draft: event.draft })) + Edit: { target: targets2.root.flow.section.Editing, from: ({ event }) => ({ draft: event.draft }) } } } } @@ -207,8 +229,8 @@ const historyMachine = Machine.make({ }, away: { on: { - ResumeShallow: (to) => to.history.flow.recent.resolve(({ target }) => target()), - ResumeDeep: (to) => to.history.flow.exact.resolve(({ target }) => target()) + ResumeShallow: { branches: "transition3", resolve: ({ select: { destination: target } }) => target() }, + ResumeDeep: { branches: "transition4", resolve: ({ select: { destination: target } }) => target() } } } } diff --git a/packages/effect-machine/test/machine/Targets.test.ts b/packages/effect-machine/test/machine/Targets.test.ts new file mode 100644 index 00000000..d13f16d7 --- /dev/null +++ b/packages/effect-machine/test/machine/Targets.test.ts @@ -0,0 +1,31 @@ +import { assert, describe, it } from "@effect/vitest" +import { Machine } from "../../src/index.js" + +describe("Machine.targets", () => { + it("preserves state names that also look like reference metadata", () => { + const root = Machine.state({ + initial: "root", + states: { + root: { + initial: "path", + states: { path: {}, from: {}, kind: {} } + } + } + }) + const targets = Machine.targets(root) + assert.deepStrictEqual(Object.keys(targets.root), ["root"]) + assert.deepStrictEqual(Object.keys(targets.root.root), ["path", "from", "kind"]) + assert.notStrictEqual(targets.root.root.path, targets.root.root.from) + assert.isTrue(Object.isFrozen(targets)) + assert.isTrue(Object.isFrozen(targets.root.root.path)) + assert.isFalse(Reflect.set(targets.root.root, "path", targets.root.root.kind)) + }) + + it("keeps reused subtrees independently addressable", () => { + const slot = Machine.state({ initial: "Idle", states: { Idle: {}, Done: {} } }) + const root = Machine.state({ initial: "Left", states: { Left: slot, Right: slot } }) + const targets = Machine.targets(root) + assert.notStrictEqual(targets.root.Left, targets.root.Right) + assert.notStrictEqual(targets.root.Left.Idle, targets.root.Right.Idle) + }) +}) diff --git a/packages/effect-machine/test/machine/Totality.test.ts b/packages/effect-machine/test/machine/Totality.test.ts index b6f7eb92..207d5347 100644 --- a/packages/effect-machine/test/machine/Totality.test.ts +++ b/packages/effect-machine/test/machine/Totality.test.ts @@ -257,6 +257,7 @@ describe("machine operation totality", () => { Done: { schema: Done, type: "final", output: Schema.NumberFromString } } }) + const targets1 = Machine.targets(states) const machine = Machine.make({ root: states, events: Machine.eventsFromSchemas(Finish), @@ -266,7 +267,7 @@ describe("machine operation totality", () => { states: { Value: { on: { - Finish: (to) => to.branch.Done().resolve(({ target }) => target.decoded({ _tag: "Done" })) + Finish: { target: targets1.root.Done, decoded: () => ({ _tag: "Done" }) } } }, Done: { output: () => 42 } diff --git a/packages/effect-machine/test/machine/TransitionConstruction.test.ts b/packages/effect-machine/test/machine/TransitionConstruction.test.ts index 9720e023..acaeb091 100644 --- a/packages/effect-machine/test/machine/TransitionConstruction.test.ts +++ b/packages/effect-machine/test/machine/TransitionConstruction.test.ts @@ -10,8 +10,11 @@ const events = Machine.events({ Reset: {}, Change: { allowed: Schema.Boolean } }) +const root1 = Machine.state({ schema: Root, initial: "Idle", states: { Idle: {}, Saved: { schema: Saved } } }) +const targets1 = Machine.targets(root1) const definition = Machine.make({ - root: Machine.state({ schema: Root, initial: "Idle", states: { Idle: {}, Saved: { schema: Saved } } }), + branches: { rootUpdate: { updated: { update: targets1.root } } }, + root: root1, events, initial: (root) => root.from(() => ({ count: 0 })) }) @@ -24,11 +27,14 @@ describe("transition construction", () => { states: { Idle: { on: { - Save: (to) => - to.local.Saved().updating(to.root).from(({ event }) => ({ + Save: { + target: targets1.root.Saved, + update: targets1.root, + from: ({ event }) => ({ target: { text: event.text }, update: { count: event.count } - })) + }) + } } }, Saved: { @@ -55,8 +61,11 @@ describe("transition construction", () => { states: { Idle: { on: { - Save: (to) => - to.local.Saved().updating(to.root).decoded(() => ({ target: targetValue, update: rootValue })) + Save: { + target: targets1.root.Saved, + update: targets1.root, + decoded: () => ({ target: targetValue, update: rootValue }) + } } } } @@ -73,10 +82,14 @@ describe("transition construction", () => { Effect.gen(function*() { const machine = definition.handle({ states: { - Idle: { on: { Save: (to) => to.local.Saved().from(({ event }) => ({ text: event.text })) } }, + Idle: { on: { Save: { target: targets1.root.Saved, from: ({ event }) => ({ text: event.text }) } } }, Saved: { on: { - Reset: (to) => to.local.Idle().updating(to.root).from(() => ({ target: undefined, update: { count: 5 } })) + Reset: { + target: targets1.root.Idle, + update: targets1.root, + from: () => ({ target: undefined, update: { count: 5 } }) + } } } } @@ -97,11 +110,14 @@ describe("transition construction", () => { states: { Idle: { on: { - Save: (to) => - to.local.Saved().updating(to.root).from(() => ({ + Save: { + target: targets1.root.Saved, + update: targets1.root, + from: () => ({ target: { text: invalidOwner ? "valid" : 42 as unknown as string }, update: { count: invalidOwner ? "invalid" as unknown as number : 1 } - })) + }) + } } }, Saved: { @@ -126,25 +142,29 @@ describe("transition construction", () => { for (const combined of [false, true]) { let constructed = 0 const machine = definition.handle({ - on: { Change: (to) => to.self.update.from(({ current }) => ({ count: current.count + 10 })) }, + on: { Change: { update: targets1.root, from: ({ root: current }) => ({ count: current.count + 10 }) } }, states: { Idle: { on: { - Change: (to) => - combined - ? to.local.Saved().updating(to.root).guard(({ current, event }) => - current.count === 0 && event.allowed - ).from(() => { + Change: combined ? + { + target: targets1.root.Saved, + update: targets1.root, + guard: ({ root, event }) => root.count === 0 && event.allowed, + from: () => { constructed++ return { target: { text: "saved" }, update: { count: 1 } } - }) - : to.root.update.guard(({ current, event }) => current.count === 0 && event.allowed).resolve( - ({ owner }, enqueue) => { - constructed++ - enqueue.raise(events.Reset()) - return owner.from({ count: 1 }) - } - ) + } + } : + { + branches: "rootUpdate", + guard: ({ root, event }) => root.count === 0 && event.allowed, + resolve: ({ select }, enqueue) => { + constructed++ + enqueue.raise(events.Reset()) + return select.updated.from({ count: 1 }) + } + } } } } @@ -171,7 +191,7 @@ describe("transition construction", () => { lifecycle.push("root") }, states: { - Idle: { on: { Save: (to) => to.local.Saved().from(({ event }) => ({ text: event.text })) } }, + Idle: { on: { Save: { target: targets1.root.Saved, from: ({ event }) => ({ text: event.text }) } } }, Saved: { entry: () => { lifecycle.push("enter") @@ -180,16 +200,23 @@ describe("transition construction", () => { lifecycle.push("exit") }, on: { - Change: (to) => - to.local.Saved().updating(to.root).reenter().guard(({ event }) => event.allowed) - .from(({ current, state }) => ({ - target: { text: state.text }, - update: { count: current.count + 1 } - })), - Reset: (to) => - to.none.reenter().resolve((_, enqueue) => { - enqueue.raise(events.Change({ allowed: false })) + Change: { + target: targets1.root.Saved, + update: targets1.root, + reenter: true, + guard: ({ event }) => event.allowed, + from: ({ root: current, state }) => ({ + target: { text: state.text }, + update: { count: current.count + 1 } }) + }, + Reset: { + none: true, + reenter: true, + resolve: (_, enqueue) => { + enqueue.raise(events.Change({ allowed: false })) + } + } } } } @@ -217,7 +244,7 @@ describe("transition construction", () => { entry: () => { entered++ }, - on: { Change: (to) => to.none.reenter().guard(({ event }) => event.allowed) } + on: { Change: { none: true, reenter: true, guard: ({ event }) => event.allowed } } } } }) @@ -232,18 +259,16 @@ describe("transition construction", () => { assert.strictEqual(accepted.next.state.path, "Idle") })) - it("rejects removed resolver options instead of silently ignoring reentry", () => { + it("rejects conflicting construction methods", () => { assert.throws(() => definition.handle({ states: { Idle: { on: { - Reset: (to) => - // @ts-expect-error reentry is a modifier, not a resolver option - to.none.resolve(() => undefined, { reenter: true }) + Reset: { none: true, resolve: () => undefined, from: () => undefined } as any } } } - }), /Use .reenter\(\)/) + }), /construction methods are mutually exclusive/) }) }) diff --git a/packages/effect-machine/test/machine/Visualization.test.ts b/packages/effect-machine/test/machine/Visualization.test.ts index 234a7e59..b97015b5 100644 --- a/packages/effect-machine/test/machine/Visualization.test.ts +++ b/packages/effect-machine/test/machine/Visualization.test.ts @@ -87,7 +87,17 @@ const initialWorkflow = (): Machine.Machine.CompleteSnapshotContaining< "application.workflow" > => initial +const targets1 = Machine.targets(States) const machineDefinition = Machine.make({ + branches: { + start: { + running: { target: targets1.root.application.workflow.running, update: targets1.root.application.workflow } + }, + unsafe: { disabled: { target: targets1.root.disabled } }, + transition1: { destination: { update: targets1.root.application.workflow } }, + transition2: { destination: { target: targets1.root.application.connection.offline } } + }, + id: "inspection-example", root: States, events: Machine.eventsFromSchemas(Start, Disconnect, Refresh), @@ -110,20 +120,23 @@ const makeMachine = (unsafeStart = false) => idle: { on: { Start: unsafeStart ? - (to) => - to.branch.disabled().resolve(({ target }) => - ({ ...target.decoded(new Disabled({})), path: "application.workflow.idle" }) as any - ) : - (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({}))) - ), - Refresh: (to) => to.local.update.resolve(({ owner }) => owner.decoded(new Workflow({}))) + { + branches: "unsafe", + resolve: ({ select }) => + ({ + ...select.disabled.decoded(new Disabled({})), + result: { path: "application.workflow.idle", value: new Disabled({}) } + }) as any + } : + { + branches: "start", + resolve: ({ select }) => + select.running.decoded( + new Running({}), + (running) => running.editing.decoded(new Editing({})) + ).update.decoded(new Workflow({})) + }, + Refresh: { update: targets1.root.application.workflow, decoded: () => (new Workflow({})) } } }, running: { @@ -135,7 +148,7 @@ const makeMachine = (unsafeStart = false) => states: { online: { on: { - Disconnect: (to) => to.local.offline().resolve(({ target }) => target.decoded(new Offline({}))) + Disconnect: { target: targets1.root.application.connection.offline, decoded: () => (new Offline({})) } } } } @@ -168,7 +181,13 @@ const LifecycleStates = Machine.state({ } }) +const targets2 = Machine.targets(LifecycleStates) const lifecycleDefinition = Machine.make({ + branches: { + transition1: { destination: { target: targets2.root.workflow } }, + transition2: { destination: { target: targets2.root.disabled } } + }, + id: "lifecycle-inspection", root: LifecycleStates, events: Machine.eventsFromSchemas(), @@ -179,18 +198,26 @@ const makeLifecycleMachine = (unsafe: "always" | "done" | undefined = undefined) lifecycleDefinition.handle({ states: { idle: { - always: (to) => - to.branch.workflow().resolve(({ target }) => { + always: { + branches: "transition1", + resolve: ({ select: { destination: target } }) => { const selected = target.decoded(new Workflow({}), (workflow) => workflow.complete.decoded(new Complete({}))) - return unsafe === "always" ? ({ ...selected, path: "idle" } as any) : selected - }) + return unsafe === "always" + ? ({ ...selected, result: { path: "idle", value: new Running({}) } } as unknown as typeof selected) + : selected + } + } }, workflow: { - onDone: (to) => - to.branch.disabled().resolve(({ target }) => { + onDone: { + branches: "transition2", + resolve: ({ select: { destination: target } }) => { const selected = target.decoded(new Disabled({})) - return unsafe === "done" ? ({ ...selected, path: "workflow" } as any) : selected - }) + return unsafe === "done" + ? ({ ...selected, result: { path: "workflow", value: new Disabled({}) } } as unknown as typeof selected) + : selected + } + } } } }) @@ -260,9 +287,11 @@ describe("Machine structural visualization", () => { reenter: false, acceptance: "required", branches: [{ - type: "direct", + type: "branch", + key: "running", + title: "running", target: "application.workflow.running", - selection: { path: "application.workflow.running", kind: "state", scope: "local" }, + selection: { path: "application.workflow.running", kind: "state", scope: "branch" }, updates: ["application.workflow"] }] }, @@ -274,7 +303,7 @@ describe("Machine structural visualization", () => { branches: [{ type: "direct", target: undefined, - selection: { path: "application.workflow", kind: "update", scope: "local" }, + selection: { path: "application.workflow", kind: "update", scope: "branch" }, updates: ["application.workflow"] }] }, @@ -286,7 +315,7 @@ describe("Machine structural visualization", () => { branches: [{ type: "direct", target: "application.connection.offline", - selection: { path: "application.connection.offline", kind: "state", scope: "local" }, + selection: { path: "application.connection.offline", kind: "state", scope: "branch" }, updates: [] }] } @@ -294,8 +323,9 @@ describe("Machine structural visualization", () => { }) it("describes reentry, eventless, and completion handlers", () => { + const root3 = Machine.state({ initial: "idle", states: { idle: Idle } }) const metadataMachine = Machine.make({ - root: Machine.state({ initial: "idle", states: { idle: Idle } }), + root: root3, events: Machine.eventsFromSchemas(Refresh), initialConfiguration: (to) => to.resolve(() => ({ @@ -307,10 +337,10 @@ describe("Machine structural visualization", () => { states: { idle: { on: { - Refresh: (to) => to.none.reenter().resolve(() => undefined) + Refresh: { none: true, reenter: true, resolve: () => undefined } }, - always: (to) => to.none, - onDone: (to) => to.none + always: { none: true }, + onDone: { none: true } } } }) @@ -370,7 +400,9 @@ describe("Machine structural visualization", () => { reenter: false, acceptance: "required", branches: [{ - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "workflow", selection: { path: "workflow", kind: "state", scope: "branch" }, updates: [] @@ -382,7 +414,9 @@ describe("Machine structural visualization", () => { reenter: false, acceptance: "required", branches: [{ - type: "direct", + type: "branch", + key: "destination", + title: "destination", target: "disabled", selection: { path: "disabled", kind: "state", scope: "branch" }, updates: [] @@ -398,10 +432,10 @@ describe("Machine structural visualization", () => { "└─ ● (root) [compound, initial: idle]", " ├─ ○ idle", " │ └─ ◇ always", - " │ └┄ → workflow", + " │ └┄ [destination] → workflow", " ├─ ○ workflow [compound, initial: complete]", " │ ├─ ◇ done", - " │ │ └┄ → disabled", + " │ │ └┄ [destination] → disabled", " │ └─ ○ complete [final]", " └─ ● disabled", "", @@ -422,7 +456,7 @@ describe("Machine structural visualization", () => { " │ ├─ ● workflow [compound, initial: idle]", " │ │ ├─ ● idle", " │ │ │ ├─ ◇ on: Start", - " │ │ │ │ └┄ → running / update application.workflow", + " │ │ │ │ └┄ [running] → running / update application.workflow", " │ │ │ └─ ◇ on: Refresh", " │ │ │ └┄ update application.workflow", " │ │ ├─ ○ running [compound, initial: editing]", @@ -451,7 +485,7 @@ describe("Machine structural visualization", () => { assert.include(rendered, "state_6 --> [*]") assert.include(rendered, "state \"○ recent [history: shallow]\" as state_7") assert.include(rendered, "[*] --> state_1") - assert.include(rendered, "state_3 --> state_4: Start / update application.workflow") + assert.include(rendered, "state_3 --> state_4: Start [running] / update application.workflow") assert.include(rendered, "state_3: Refresh / update application.workflow") assert.include(rendered, "state_9 --> state_10: Disconnect") assert.notMatch(rendered, /state_\d+ --> state_\d+: Refresh/) diff --git a/packages/effect-machine/test/testing/Coverage.test.ts b/packages/effect-machine/test/testing/Coverage.test.ts index 00e3886e..c9f4a7d7 100644 --- a/packages/effect-machine/test/testing/Coverage.test.ts +++ b/packages/effect-machine/test/testing/Coverage.test.ts @@ -17,7 +17,10 @@ class Finish extends Schema.TaggedClass("Finish")("Finish", {}) {} const CounterStates = Machine.state({ initial: "count", states: { count: Count, done: Done } }) +const targets1 = Machine.targets(CounterStates) const counterMachine = Machine.make({ + branches: { transition1: { destination: { target: targets1.root.count } } }, + root: CounterStates, events: Machine.eventsFromSchemas(Add, Finish), initialConfiguration: (root) => @@ -26,12 +29,14 @@ const counterMachine = Machine.make({ states: { count: { on: { - Add: (to) => - to.branch.count().reenter().resolve( - ({ event, state, target }) => target.decoded(new Count({ value: state.value + event.amount })), - { declinable: true } - ), - Finish: (to) => to.branch.done().resolve(({ target }) => target.decoded(new Done({}))) + Add: { + branches: "transition1", + reenter: true, + resolve: ({ event, state, select: { destination: target } }) => + target.decoded(new Count({ value: state.value + event.amount })), + declinable: true + }, + Finish: { target: targets1.root.done, decoded: () => (new Done({})) } } }, done: {} @@ -52,7 +57,12 @@ const opaqueMachine = Machine.make({ }) const StartupStates = Machine.state({ initial: "count", states: { count: Count } }) +const targets2 = Machine.targets(StartupStates) const startupMachine = Machine.make({ + branches: { + transition1: { zero: { target: targets2.root.count, title: "Count is zero" }, unchanged: { none: true } } + }, + root: StartupStates, events: Machine.eventsFromSchemas(Add), initialConfiguration: (root) => @@ -60,20 +70,18 @@ const startupMachine = Machine.make({ }).handle({ states: { count: { - always: (to) => - to.branches({ - zero: { title: "Count is zero", target: to.branch.count() }, - unchanged: { target: to.none } - }).resolve(({ state, select }) => + always: { + branches: "transition1", + resolve: ({ state, select }) => state.value === 0 ? select.zero.decoded(new Count({ value: 1 })) : select.unchanged() - ), + }, on: { - Add: (to) => - to.branch.count().resolve(({ event, state, target }) => - target.decoded(new Count({ value: state.value + event.amount })) - ) + Add: { + target: targets2.root.count, + decoded: ({ event, state }) => (new Count({ value: state.value + event.amount })) + } } } } @@ -84,6 +92,8 @@ class Select extends Schema.TaggedClass