diff --git a/.changeset/typed-operation-boundary.md b/.changeset/typed-operation-boundary.md new file mode 100644 index 00000000..dbb835e8 --- /dev/null +++ b/.changeset/typed-operation-boundary.md @@ -0,0 +1,7 @@ +--- +"@typeonce/effect-machine": patch +--- + +Check the public `Machine` operations against their implementations at compile time. + +`Machine.start`, `resume`, `plan`, `planInitial`, `can`, `enabled`, `isFinal`, `encodeSnapshot`, `decodeSnapshot`, `make`, `targets`, and the event protocol builders no longer rely on unchecked casts, so their documented signatures now stay in sync with runtime behavior. Planning failures are classified consistently across runtime strategies: non-stabilization and schema failures stay typed, startup throws become `StartupError`, and other handler throws remain defects. diff --git a/packages/effect-machine/src/Machine.ts b/packages/effect-machine/src/Machine.ts index 86649b8a..7ac24cbd 100644 --- a/packages/effect-machine/src/Machine.ts +++ b/packages/effect-machine/src/Machine.ts @@ -2613,7 +2613,6 @@ export declare namespace Machine { /** @internal */ readonly stateNodes: StateNodes /** @internal */ - /** @internal */ readonly handlers: any /** @internal */ readonly initial: any @@ -7737,7 +7736,7 @@ export const isFinal: < ParentEvents >, state: Machine.Snapshot -) => state is Machine.SnapshotContainingFinal = internal.isFinal as any +) => state is Machine.SnapshotContainingFinal = internal.isFinal /** * Defines root or nested state schemas while preserving the exact child topology. @@ -7835,10 +7834,8 @@ export interface Targets { * @category constructors * @since 0.34.0 */ -export const targets: (root: State) => Targets = internal - .targets as unknown as ( - root: State - ) => Targets +export const targets: (root: State) => Targets = + internal.targets type UniqueSourceNames = [ | Extract @@ -8066,7 +8063,7 @@ interface Make { * @category constructors * @since 0.4.0 */ -export const make: Make = internal.make as unknown as Make +export const make: Make = internal.make /** * Extracts the decoded event union carried by an event protocol descriptor. @@ -8131,7 +8128,7 @@ type ValidateEventFields = { */ export const events: >>( cases: Cases & ValidateEventFields> & ValidateEventProtocolBuilder<"public", EventFieldsSchemas> -) => Machine.EventProtocol<"public", EventFieldsSchemas> = internal.eventsFromFields as any +) => Machine.EventProtocol<"public", EventFieldsSchemas> = internal.eventsFromFields /** Imports existing schemas and protocols without rebuilding their contracts. * @category constructors @@ -8141,7 +8138,7 @@ export const eventsFromSchemas: { >>( ...inputs: Inputs & ValidateEventProtocolBuilder<"public", Inputs> ): Machine.EventProtocol<"public", Machine.EventProtocolInputSchemasOf<"public", Inputs>> -} = internal.events as any +} = internal.events /** * Requires the machine to run as an owned child whose parent accepts the @@ -8201,7 +8198,7 @@ export const internalEvents: > & ValidateEventProtocolBuilder<"internal", EventFieldsSchemas> -) => Machine.EventProtocol<"internal", EventFieldsSchemas> = internal.internalEventsFromFields as any +) => Machine.EventProtocol<"internal", EventFieldsSchemas> = internal.internalEventsFromFields /** Imports existing schemas and protocols without rebuilding their contracts. * @category constructors @@ -8211,7 +8208,7 @@ export const internalEventsFromSchemas: { >>( ...inputs: Inputs & ValidateEventProtocolBuilder<"internal", Inputs> ): Machine.EventProtocol<"internal", Machine.EventProtocolInputSchemasOf<"internal", Inputs>> -} = internal.internalEvents as any +} = internal.internalEvents /** * Defines the ephemeral notifications a machine may publish to external @@ -8238,7 +8235,7 @@ export const emittedEvents: > & ValidateEventProtocolBuilder<"emitted", EventFieldsSchemas> -) => Machine.EventProtocol<"emitted", EventFieldsSchemas> = internal.emittedEventsFromFields as any +) => Machine.EventProtocol<"emitted", EventFieldsSchemas> = internal.emittedEventsFromFields /** Imports existing schemas and protocols without rebuilding their contracts. * @category constructors @@ -8248,7 +8245,7 @@ export const emittedEventsFromSchemas: { >>( ...inputs: Inputs & ValidateEventProtocolBuilder<"emitted", Inputs> ): Machine.EventProtocol<"emitted", Machine.EventProtocolInputSchemasOf<"emitted", Inputs>> -} = internal.emittedEvents as any +} = internal.emittedEvents /** * Encodes a decoded machine snapshot into a normalized data representation. @@ -8342,7 +8339,7 @@ export const encodeSnapshot: < Machine.EncodedSnapshot, MachineSchemaEncodeError, Machine.SnapshotEncodingServices -> = internal.encodeSnapshot as any +> = internal.encodeSnapshot /** * Decodes a normalized data representation into a validated machine snapshot. @@ -8430,7 +8427,7 @@ export const decodeSnapshot: < Machine.Snapshot, MachineSchemaDecodeError, Machine.SnapshotDecodingServices -> = internal.decodeSnapshot as any +> = internal.decodeSnapshot type TransitionBranchRecordError = { readonly "~effect/Machine/TransitionBranchRecordError": Message @@ -8578,7 +8575,7 @@ export const planInitial: < ), InitialE | E | InfiniteTransitionError | MachineSchemaDecodeError | StartupError, never -> = internal.planInitial as any +> = internal.planInitial /** * Returns every compiled state node in definition order. @@ -8738,7 +8735,7 @@ export const enabled: < ParentEvents >, state: Machine.Snapshot -) => ReadonlyArray> = internal.enabled as any +) => ReadonlyArray> = internal.enabled /** * Tests whether a concrete event would select at least one transition from a @@ -8870,7 +8867,7 @@ export const can: { state: Machine.Snapshot, event: Machine.EventInputOf ): Effect.Effect -} = internal.can as any +} = internal.can /** * Returns an Effect that plans the next state snapshot without running command effects. @@ -9004,7 +9001,7 @@ export const plan: < ), E | InfiniteTransitionError | MachineSchemaDecodeError, never -> = internal.plan as any +> = internal.plan /** * Creates advanced stateful process logic from explicit initialization and @@ -9315,7 +9312,7 @@ export const prepare: < Machine.EmittedEventOf > > -> = internal.prepare as any +> = internal.prepare /** * Starts a machine. @@ -9429,7 +9426,7 @@ export const start: < Machine.EventOf, Machine.EmittedEventOf > -> = internal.start as any +> = internal.start /** * Starts a fresh managed runtime from a decoded logical snapshot. @@ -9541,4 +9538,4 @@ export const resume: < Machine.EventOf, Machine.EmittedEventOf > -> = internal.resume as any +> = internal.resume diff --git a/packages/effect-machine/src/internal/machine/atom.ts b/packages/effect-machine/src/internal/machine/atom.ts index 352b2e1f..77792938 100644 --- a/packages/effect-machine/src/internal/machine/atom.ts +++ b/packages/effect-machine/src/internal/machine/atom.ts @@ -140,7 +140,7 @@ const resumeMachineAtomEffect = ( get: Atom.AtomContext, machine: Machine.Machine.Any, snapshot: Machine.Machine.Snapshot -) => runMachineAtomEffect(get, internalMachine.resume(machine as any, snapshot as any)) +) => runMachineAtomEffect(get, internalMachine.resume(machine, snapshot)) type RefState = Ref extends Machine.MachineRef ? State : never type RefError = Ref extends Machine.MachineRef ? Error : never @@ -723,10 +723,7 @@ export const can = (event: unknown) => { event: unknown ) => Effect.Effect = machine === undefined ? () => Effect.die(new Error("AtomMachine.can requires a machine atom created by AtomMachine")) - : internalMachine.can(machine) as ( - state: Machine.Machine.Snapshot, - event: unknown - ) => Effect.Effect + : internalMachine.can(machine) const result = Atom.readable((get): AsyncResult.AsyncResult => { const current = get(self.snapshot) @@ -840,7 +837,7 @@ export const make: { Machine.Machine.EmittedEventOf > } = ((machine: Machine.Machine.Any, ...args: ReadonlyArray) => { - const prepared = Atom.make(() => internalMachine.prepare(machine as any, ...(args as []))) + const prepared = Atom.make(() => internalMachine.prepare(machine, ...args)) const ref = Atom.make((get) => startPreparedMachineAtomEffect(get, prepared as any)) const result = makeFromRefAtom(ref as any, machine) preparedByMachineAtom.set(result, prepared as any) @@ -869,7 +866,7 @@ const makeWithRuntime = ( machine: Machine.Machine.Any, args: ReadonlyArray ): MachineAtom => { - const prepared = runtime.atom(() => internalMachine.prepare(machine as any, ...(args as []))) + const prepared = runtime.atom(() => internalMachine.prepare(machine, ...args)) const ref = runtime.atom((get) => startPreparedMachineAtomEffect(get, prepared as any)) const result = makeFromRefAtom(ref as any, machine) preparedByMachineAtom.set(result, prepared as any) diff --git a/packages/effect-machine/src/internal/machine/declaration.ts b/packages/effect-machine/src/internal/machine/declaration.ts index 843e7702..006f4466 100644 --- a/packages/effect-machine/src/internal/machine/declaration.ts +++ b/packages/effect-machine/src/internal/machine/declaration.ts @@ -2,7 +2,7 @@ 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 type { Machine } from "../../Machine.js" import * as Reference from "./targetReference.js" import * as Topology from "./topology.js" @@ -13,7 +13,7 @@ interface Source { } export interface Declaration { readonly initialize?: (input: unknown) => unknown - readonly root: State + readonly root: Machine.Any["root"] readonly sources: ReadonlyMap readonly branches: ReadonlyMap>>>> } @@ -22,7 +22,7 @@ const record = (value: unknown, message: string): Record => { return value as Record } export const capture = ( - root: State, + root: Machine.Any["root"], config: Readonly> ): Declaration => { const sources = new Map() diff --git a/packages/effect-machine/src/internal/machine/errors.ts b/packages/effect-machine/src/internal/machine/errors.ts index 1b0c0416..6a272425 100644 --- a/packages/effect-machine/src/internal/machine/errors.ts +++ b/packages/effect-machine/src/internal/machine/errors.ts @@ -1,5 +1,6 @@ -import type * as Cause from "effect/Cause" +import * as Cause from "effect/Cause" import * as Data from "effect/Data" +import * as Effect from "effect/Effect" import type * as Schema from "effect/Schema" /** @@ -85,3 +86,23 @@ export class ProcessLocalError extends Data.TaggedError("ProcessLocalError")<{ * @since 0.4.0 */ export class StoppedError extends Data.TaggedError("StoppedError") {} + +/** + * Failures that synchronous planning reports through the typed error channel. + * Every other planning throw is a defect. + */ +export type PlanningError = InfiniteTransitionError | MachineSchemaDecodeError + +export const isPlanningError = (error: unknown): error is PlanningError => + error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError + +/** Converts a synchronous planning throw into a typed failure or a defect. */ +export const failPlanning = (error: unknown): Effect.Effect => + isPlanningError(error) ? Effect.fail(error) : Effect.die(error) + +/** + * Classifies a synchronous initial-planning throw. Planning failures stay + * typed; any other throw is preserved as the defect cause of a `StartupError`. + */ +export const toStartupFailure = (error: unknown): PlanningError | StartupError => + isPlanningError(error) || error instanceof StartupError ? error : new StartupError({ cause: Cause.die(error) }) diff --git a/packages/effect-machine/src/internal/machine/initialDeclaration.ts b/packages/effect-machine/src/internal/machine/initialDeclaration.ts index 25a06e8f..a0a7426d 100644 --- a/packages/effect-machine/src/internal/machine/initialDeclaration.ts +++ b/packages/effect-machine/src/internal/machine/initialDeclaration.ts @@ -1,7 +1,7 @@ /** Captures handler-owned initial edges before compiling executable topology. */ import { hasProperty } from "effect/Predicate" import * as Schema from "effect/Schema" -import type { Machine, State } from "../../Machine.js" +import type { Machine } from "../../Machine.js" import { SnapshotBuilderStateTypeId } from "./symbols.js" import * as Reference from "./targetReference.js" import * as Topology from "./topology.js" @@ -52,7 +52,7 @@ interface Captured { readonly handlers: Record } export const capture = ( - root: State, + root: Machine.Any["root"], handler: Readonly> ): Captured => { const visit = ( diff --git a/packages/effect-machine/src/internal/machine/machine.ts b/packages/effect-machine/src/internal/machine/machine.ts index 2569a766..e3185c33 100644 --- a/packages/effect-machine/src/internal/machine/machine.ts +++ b/packages/effect-machine/src/internal/machine/machine.ts @@ -7,13 +7,9 @@ import * as Schema from "effect/Schema" import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" import type { - ActionError, ChildAddress, ChildMachine, - Command, Definition, - ExecutionServices, - InitialEvent as InitialEventModel, Logic, Machine, MachineRef, @@ -30,7 +26,7 @@ import * as Activities from "./activities.js" import * as Configuration from "./configuration.js" import * as Construction from "./construction.js" import * as Declaration from "./declaration.js" -import type { ChildAlreadyExistsError, InfiniteTransitionError, StartupError } from "./errors.js" +import type { ChildAlreadyExistsError, PlanningError, StartupError } from "./errors.js" import type { CapturedStateConfig } from "./implementation.js" import * as InitialDeclaration from "./initialDeclaration.js" import * as InvocationDefinition from "./invocationDefinition.js" @@ -38,8 +34,6 @@ import * as Observation from "./observation.js" import * as internalPlanner from "./planner.js" import * as internalProcess from "./process.js" import * as Protocol from "./protocol.js" -import type { EnsureExecutable } from "./readiness.js" -import type { ExcludeCompatibleRuntime } from "./requirements.js" import * as internalRuntime from "./runtimeProtocol.js" import * as Serialization from "./serialization.js" import * as StateDefinition from "./stateDefinition.js" @@ -60,8 +54,7 @@ export { ChildMachineLogicTypeId, InitialEventTypeId, SnapshotBuilderStateTypeId /** 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 +export const targets: (root: Machine.Any["root"]) => any = TargetReference.make const TypeId = "~effect/Machine" const ParentTypeId = "~effect/Machine/Parent" @@ -742,38 +735,11 @@ export const isMachine = ( u: unknown ): u is Machine.Any => hasProperty(u, TypeId) && u[TypeId] === TypeId -export const isFinal = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - >, - state: Machine.Snapshot -): state is Machine.SnapshotContainingFinal => internalPlanner.isFinal(machine as any, state) +// Erased machine operations. `any` marks the positions whose types the public +// `Machine` module computes from the machine's generic parameters; the public +// module specializes each operation without a cast. + +export const isFinal: (machine: Machine.Any, state: any) => state is any = internalPlanner.isFinal export const state = (node: unknown): State => { const captured = StateDefinition.captureRoot(node) as Machine.StateNodeConfig @@ -806,54 +772,23 @@ const makeStateHelpers = (): Machine.StateAccessors<{ readonly "": Machine.State } } -type MakeResult< - States extends Machine.StateSchemas, - InputEvents extends ReadonlyArray, - Emits extends ReadonlyArray, - Input extends Schema.Top, - InitialE, - InitialR, - InternalEvents extends ReadonlyArray, - ParentDeclaration extends Parent.Any | undefined -> = Definition< - States, - readonly [...InputEvents, ...InternalEvents], - Input, - InitialE, - InitialR, - Machine.FinalStateFromDefinition, - Machine.TerminalOutput, - Emits, - InputEvents, - Machine.ParentEventsOf -> - -export const make = < - const States extends Machine.StateSchemas, - const InputEvents extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - InitialE = never, - InitialR = never, - const InternalEvents extends ReadonlyArray = readonly [], - const ParentDeclaration extends Parent.Any | undefined = undefined ->( +export const make = ( config: { readonly id?: string - readonly root: State + readonly root: Machine.Any["root"] readonly effects?: unknown readonly streams?: unknown readonly timers?: unknown readonly logic?: unknown readonly children?: unknown readonly branches?: unknown - readonly events: Machine.EventProtocol<"public", InputEvents> - readonly internalEvents?: Machine.EventProtocol<"internal", InternalEvents> - readonly emittedEvents?: Machine.EventProtocol<"emitted", Emits> - readonly parent?: ParentDeclaration - readonly input?: Input + readonly events: Machine.EventProtocol<"public", any> + readonly internalEvents?: Machine.EventProtocol<"internal", any> + readonly emittedEvents?: Machine.EventProtocol<"emitted", any> + readonly parent?: Parent.Any | undefined + readonly input?: Schema.Top } -): MakeResult => { +): any => { if (Object.hasOwn(config, "initial") || Object.hasOwn(config, "initialConfiguration")) { throw new Error("Machine initial edges and root data belong in handle") } @@ -900,12 +835,15 @@ const eventFieldSchemas = ( return Object.keys(cases).length === 0 ? [] : [Schema.TaggedUnion(cases)] } -export const eventsFromFields = (cases: Readonly>) => - Protocol.makeEventProtocol("public", eventFieldSchemas(cases)) -export const internalEventsFromFields = (cases: Readonly>) => - Protocol.makeEventProtocol("internal", eventFieldSchemas(cases)) -export const emittedEventsFromFields = (cases: Readonly>) => - Protocol.makeEventProtocol("emitted", eventFieldSchemas(cases)) +export const eventsFromFields = ( + cases: Readonly> +): Machine.EventProtocol<"public", any> => Protocol.makeEventProtocol("public", eventFieldSchemas(cases)) +export const internalEventsFromFields = ( + cases: Readonly> +): Machine.EventProtocol<"internal", any> => Protocol.makeEventProtocol("internal", eventFieldSchemas(cases)) +export const emittedEventsFromFields = ( + cases: Readonly> +): Machine.EventProtocol<"emitted", any> => Protocol.makeEventProtocol("emitted", eventFieldSchemas(cases)) const makeParent = < const Mode extends ParentMode, @@ -944,151 +882,20 @@ export const emittedEvents = > -export const encodeSnapshot: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - >, - snapshot: Machine.Snapshot -) => Effect.Effect< - Machine.EncodedSnapshot, - MachineSchemaEncodeError, - Machine.SnapshotEncodingServices -> = Serialization.encodeSnapshot as any - -export const decodeSnapshot: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - >, +export const encodeSnapshot: ( + machine: Machine.Any, + snapshot: any +) => Effect.Effect = Serialization.encodeSnapshot + +export const decodeSnapshot: ( + machine: Machine.Any, encoded: unknown -) => Effect.Effect< - Machine.Snapshot, - MachineSchemaDecodeError, - Machine.SnapshotDecodingServices -> = Serialization.decodeSnapshot as any - -export const planInitial: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: - & Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - > - & EnsureExecutable, - ...args: [...Machine.InputArgs] -) => Effect.Effect< - & { - readonly startingState: Machine.Snapshot - readonly initialEntryPaths: ReadonlyArray> - readonly state: Machine.Snapshot - readonly commands: ReadonlyArray - readonly emittedEvents: ReadonlyArray> - readonly microsteps: ReadonlyArray<{ - readonly next: Machine.Snapshot - readonly event: Machine.EventOf | InitialEventModel - readonly transitions: ReadonlyArray< - Machine.RetainedTransition< - Machine.StateNodeIdentifier, - Machine.TagOf, - Machine.StateNodeIdentifier - > - > - readonly commands: ReadonlyArray - readonly raisedEvents: ReadonlyArray> - readonly emittedEvents: ReadonlyArray> - readonly exitPaths: ReadonlyArray - readonly entryPaths: ReadonlyArray - readonly changed: boolean - }> - } - & ( - | { - readonly done: true - readonly output: Output - } - | { - readonly done: false - readonly output: undefined - } - ), - InitialE | E | InfiniteTransitionError | MachineSchemaDecodeError | StartupError, - never -> = internalPlanner.planInitial as any +) => Effect.Effect = Serialization.decodeSnapshot + +export const planInitial: ( + machine: Machine.Any, + ...args: ReadonlyArray +) => Effect.Effect = internalPlanner.planInitial export const stateNodes = ( machine: M @@ -1162,111 +969,18 @@ export const configuration = ( ) } -export const enabled = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - >, - state: Machine.Snapshot -): ReadonlyArray> => internalPlanner.enabled(machine as any, state) - -export const can = internalPlanner.can - -export const plan: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: - & Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - > - & EnsureExecutable, - state: Machine.Snapshot, - event: Machine.EventInputOf -) => Effect.Effect< - & { - readonly next: Machine.Snapshot - readonly commands: ReadonlyArray - readonly emittedEvents: ReadonlyArray> - readonly microsteps: ReadonlyArray<{ - readonly next: Machine.Snapshot - readonly event: Machine.EventOf | InitialEventModel - readonly transitions: ReadonlyArray< - Machine.RetainedTransition< - Machine.StateNodeIdentifier, - Machine.TagOf, - Machine.StateNodeIdentifier - > - > - readonly commands: ReadonlyArray - readonly raisedEvents: ReadonlyArray> - readonly emittedEvents: ReadonlyArray> - readonly exitPaths: ReadonlyArray - readonly entryPaths: ReadonlyArray - readonly changed: boolean - }> - } - & ( - | { - readonly done: true - readonly output: Output - } - | { - readonly done: false - readonly output: undefined - } - ), - E | InfiniteTransitionError | MachineSchemaDecodeError, - never -> = internalPlanner.plan as any +export const enabled: (machine: Machine.Any, state: any) => ReadonlyArray = internalPlanner.enabled + +export const can: { + (machine: Machine.Any): (state: any, event: any) => Effect.Effect + (machine: Machine.Any, state: any, event: any): Effect.Effect +} = internalPlanner.can + +export const plan: ( + machine: Machine.Any, + state: any, + event: any +) => Effect.Effect = internalPlanner.plan export const logic = < State, @@ -1316,8 +1030,8 @@ export const child = ( ): ChildMachine => makeChild(id, machine, (input) => machine.input === undefined - ? (internalProcess.toProcessLogic as any)(machine) - : (internalProcess.toProcessLogic as any)(machine, input)) + ? internalProcess.toProcessLogic(machine) + : internalProcess.toProcessLogic(machine, input)) const makeChild = ( id: Id, @@ -1333,8 +1047,8 @@ const makeChild = ( export const childFamily = (machine: M): ChildMachine.Family => { const makeLogic = (input?: unknown): Logic => machine.input === undefined - ? (internalProcess.toProcessLogic as any)(machine) - : (internalProcess.toProcessLogic as any)(machine, input) + ? internalProcess.toProcessLogic(machine) + : internalProcess.toProcessLogic(machine, input) return (id) => makeChild(id, machine, makeLogic) } @@ -1414,116 +1128,17 @@ export const watch = ( export const waitFor = Observation.waitFor -export const prepare = internalProcess.prepare - -export const start: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: - & Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - > - & EnsureExecutable, - ...args: [...Machine.InputArgs] -) => Effect.Effect< - MachineRef< - Machine.Snapshot, - Machine.EventInputOf, - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StoppedError, - Output, - Machine.EmittedEventOf - >, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError, - ExcludeCompatibleRuntime< - ExecutionServices, - Machine.EventOf, - Machine.EmittedEventOf - > -> = internalProcess.start as any - -export const resume: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never, - InputEvents extends ReadonlyArray = Events ->( - machine: - & Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates, - InputEvents - > - & EnsureExecutable, - snapshot: Machine.Snapshot -) => Effect.Effect< - MachineRef< - Machine.Snapshot, - Machine.EventInputOf, - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StoppedError, - Output, - Machine.EmittedEventOf - >, - MachineSchemaDecodeError, - ExcludeCompatibleRuntime< - ExecutionServices, - Machine.EventOf, - Machine.EmittedEventOf - > -> = internalProcess.resume as any +export const prepare: ( + machine: Machine.Any, + ...args: ReadonlyArray +) => Effect.Effect> = internalProcess.prepare + +export const start: ( + machine: Machine.Any, + ...args: ReadonlyArray +) => Effect.Effect, any, any> = internalProcess.start + +export const resume: ( + machine: Machine.Any, + snapshot: any +) => Effect.Effect, MachineSchemaDecodeError, any> = internalProcess.resume diff --git a/packages/effect-machine/src/internal/machine/planner.ts b/packages/effect-machine/src/internal/machine/planner.ts index e79f52c8..1f161cb8 100644 --- a/packages/effect-machine/src/internal/machine/planner.ts +++ b/packages/effect-machine/src/internal/machine/planner.ts @@ -4,9 +4,7 @@ * @since 0.4.0 */ -import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" -import type * as Schema from "effect/Schema" import type { Enqueue, InitialEvent as MachineInitialEvent, Machine, MachineTarget } from "../../Machine.js" import { makeCollector, type RuntimeCommand } from "./command.js" import { @@ -38,7 +36,9 @@ import { snapshotFromConfigurationAtPath } from "./configuration.js" import * as Construction from "./construction.js" -import { InfiniteTransitionError, MachineSchemaDecodeError, StartupError, StoppedError } from "./errors.js" +import { failPlanning, InfiniteTransitionError, MachineSchemaDecodeError, toStartupFailure } from "./errors.js" +import { StoppedError } from "./errors.js" +import type { PlanningError, StartupError } from "./errors.js" import { type CapturedStateConfig, toImpl } from "./implementation.js" import { isDataInitializer } from "./initialDeclaration.js" import { getStateInitializeValues, makeStateInitializeBuilder } from "./initialization.js" @@ -770,7 +770,7 @@ const makeTransitionContext = < StateId extends Machine.StateIdentifier, EventTag extends Machine.TagOf >( - machine: Machine, + machine: Machine.Any, configuration: ActiveConfiguration, path: string, event: Machine.EventByTag, @@ -1728,58 +1728,20 @@ export const getFinalOutputEffect = < }) ) -export const isFinal = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates - >, - state: Machine.Snapshot -): state is Machine.SnapshotContainingFinal => isFinalState(machine, state) - -export const planInitialSync = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - ...args: [...Machine.InputArgs] +export const isFinal = ( + machine: Machine.Any, + state: Machine.Snapshot +): state is Machine.SnapshotContainingFinal => isFinalState(machine, state) + +export const planInitialSync = ( + machine: Machine.Any, + ...args: ReadonlyArray ) => { const inputArgs = machine.input === undefined ? args : args.length === 0 ? (decodeInputSync(machine, machine.input, undefined), args) - : [decodeInputSync(machine, machine.input, args[0])] as [...Machine.InputArgs] + : [decodeInputSync(machine, machine.input, args[0])] const initial = machine.initial(...inputArgs) const emptyConfiguration: ActiveConfiguration = { active: new Set(), @@ -1789,12 +1751,12 @@ export const planInitialSync = < } const rootResolution = resolveInitialTarget(machine, emptyConfiguration, initial, InitialEvent) const configuration = normalizeTargetConfigurationSync(machine, emptyConfiguration, rootResolution.target) - const startingState = snapshotFromConfiguration(machine, configuration) + const startingState = snapshotFromConfiguration(machine, configuration) const initialEntryPaths = getInitialEntryPaths(machine, configuration) const commands = rootResolution.commands const raisedEvents = rootResolution.raisedEvents const emittedEvents = rootResolution.emittedEvents - const entry = collectStateActions( + const entry = collectStateActions( machine, configuration, initialEntryPaths, @@ -1806,7 +1768,7 @@ export const planInitialSync = < configuration, InitialEvent, [...entry.commands], - [...raisedEvents, ...entry.raisedEvents] as Array>, + [...raisedEvents, ...entry.raisedEvents], [...emittedEvents, ...entry.emittedEvents], rootResolution.transitions.length === 0 ? [] : @@ -1817,7 +1779,7 @@ export const planInitialSync = < commands, raisedEvents: [ ...raisedEvents - ] as ReadonlyArray>, + ], emittedEvents, exitPaths: [], entryPaths: [], @@ -1829,14 +1791,14 @@ export const planInitialSync = < const planned = { startingState, initialEntryPaths, - state: snapshotFromConfiguration(machine, settled.next), + state: snapshotFromConfiguration(machine, settled.next), commands: [ ...commands, ...settled.commands ], - emittedEvents: settled.emittedEvents as ReadonlyArray>, + emittedEvents: settled.emittedEvents, microsteps: settled.microsteps.map((step) => ({ - next: snapshotFromConfiguration(machine, step.next), + next: snapshotFromConfiguration(machine, step.next), event: step.event, transitions: step.transitions, commands: step.commands, @@ -1852,47 +1814,21 @@ export const planInitialSync = < : { ...planned, done: false as const, output: undefined } } -export const enabled = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray, - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - OutputStates extends Machine.StateIdentifier = never ->( - machine: Machine< - States, - Events, - Input, - UnhandledStates, - E, - R, - InitialE, - InitialR, - FinalStates, - Output, - Emits, - OutputStates - >, - state: Machine.Snapshot -): ReadonlyArray> => { +export const enabled = ( + machine: Machine.Any, + state: Machine.Snapshot +): ReadonlyArray> => { if (isFinalState(machine, state)) { return [] } const configuration = normalizeConfiguration(machine, state) - const tags: Array> = [] + const tags: Array> = [] const seen = new Set() for (const path of getCandidatePaths(machine, configuration)) { for (const tag of Reflect.ownKeys(toImpl(machine).handlers[path]?.on ?? {})) { if (!seen.has(tag)) { seen.add(tag) - tags.push(tag as Machine.TagOf) + tags.push(tag) } } } @@ -1910,24 +1846,11 @@ const canSync = ( return selectEventTransitions(machine, configuration, decodedEvent as any).length > 0 } -const microstep = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never, - Context = never ->( - machine: Machine, +const microstep = ( + machine: Machine.Any, state: ActiveConfiguration, - event: Machine.LifecycleEvent, - selections: ReadonlyArray> + event: Machine.LifecycleEvent, + selections: ReadonlyArray> ) => { if (selections.length === 0) { return { @@ -1945,10 +1868,10 @@ const microstep = < } const activeSelections = removePreemptedAncestorSelections(selections) - const evaluatedTransitions: Array, E, R, Context>> = [] + const evaluatedTransitions: Array, any, any, any>> = [] for (const selection of activeSelections) { evaluatedTransitions.push( - collectEvaluatedTransition, E, R, Context>( + collectEvaluatedTransition( machine, state, selection @@ -1994,7 +1917,7 @@ const microstep = < ] for (const transition of targetApplicationOrder) { if (transition.target !== undefined) { - stateAfterTransition = normalizeTargetConfigurationSync( + stateAfterTransition = normalizeTargetConfigurationSync( machine, stateAfterTransition, transition.target @@ -2029,14 +1952,14 @@ const microstep = < const exitPaths = sortExitPaths(machine, sortedTransitions.flatMap((transition) => transition.exitPaths)) const entryPaths = sortEntryPaths(machine, sortedTransitions.flatMap((transition) => transition.entryPaths)) stateAfterTransition = captureHistory(machine, state, stateAfterTransition, exitPaths) - const exit = collectStateActions( + const exit = collectStateActions( machine, state, exitPaths, event, "exit" ) - const entry = collectStateActions( + const entry = collectStateActions( machine, stateAfterTransition, entryPaths, @@ -2050,7 +1973,7 @@ const microstep = < transitions: retainedTransitions, commands: [...exit.commands, ...transitionActions, ...entry.commands], raisedEvents: [...exit.raisedEvents, ...transitionRaisedEvents, ...entry.raisedEvents] as ReadonlyArray< - Machine.EventOf + Machine.EventOf >, emittedEvents: [...exit.emittedEvents, ...transitionEmittedEvents, ...entry.emittedEvents], exitPaths, @@ -2060,26 +1983,14 @@ const microstep = < } } -const settle = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, +const settle = ( + machine: Machine.Any, state: ActiveConfiguration, - event: Machine.LifecycleEvent, + event: Machine.LifecycleEvent, commands: Array, - raisedEvents: Array>, + raisedEvents: Array>, emittedEvents: Array, - microsteps: Array, E, R>> + microsteps: Array, any, any>> ) => { let currentState = state let currentEvent = event @@ -2087,7 +1998,7 @@ const settle = < let iterations = 0 let raisedEventIndex = 0 let completedTerminal = false - let finalOutput: Output | undefined = undefined + let finalOutput: any | undefined = undefined const pendingCompletions: Array<{ readonly path: string; readonly output: unknown }> = [] while (true) { @@ -2108,14 +2019,14 @@ const settle = < while (pendingCompletions.length > 0 && !currentState.active.has(pendingCompletions[0]!.path)) { pendingCompletions.shift() } - const done = selectDoneTransitions( + const done = selectDoneTransitions( machine, currentState, currentEvent, pendingCompletions.length === 0 ? [] : [pendingCompletions.shift()!] ) if (done.length > 0) { - const doneStep: SettlingMicrostep, E, R> = microstep( + const doneStep: SettlingMicrostep, any, any> = microstep( machine, currentState, currentEvent, @@ -2135,15 +2046,15 @@ const settle = < throw new Error("Machine reached a terminal configuration without a completed root output") } completedTerminal = true - finalOutput = currentState.outputs.get(root) as Output + finalOutput = currentState.outputs.get(root) as any break } const always = shouldRunAlways - ? selectAlwaysTransitions(machine, currentState, currentEvent) + ? selectAlwaysTransitions(machine, currentState, currentEvent) : [] if (always.length > 0) { - const alwaysStep: SettlingMicrostep, E, R> = microstep( + const alwaysStep: SettlingMicrostep, any, any> = microstep( machine, currentState, currentEvent, @@ -2168,10 +2079,10 @@ const settle = < // internal queue, so decoding it again here only repeats schema work. const raisedEvent = raisedEventValue currentEvent = raisedEvent - const raisedSelections = selectEventTransitions( + const raisedSelections = selectEventTransitions( machine, currentState, - raisedEvent as Machine.EventByTag> + raisedEvent ) if (raisedSelections.length === 0) { shouldRunAlways = true @@ -2198,28 +2109,16 @@ const settle = < microsteps } return completedTerminal - ? { ...result, done: true as const, output: finalOutput as Output } + ? { ...result, done: true as const, output: finalOutput as any } : { ...result, done: false as const, output: undefined } } -const macrostepConfiguration = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, +const macrostepConfiguration = ( + machine: Machine.Any, configuration: ActiveConfiguration, - event: Machine.EventOf | InvocationEvent.InvocationEvent + event: Machine.EventOf | InvocationEvent.InvocationEvent ) => { - const decodedEvent = InvocationEvent.isInvocationEvent(event) ? event : decodeEventSync(machine, event) + const decodedEvent = InvocationEvent.isInvocationEvent(event) ? event : decodeEventSync(machine, event) if (isActiveFinalConfiguration(machine, configuration)) { const completed = completeConfigurationSync(machine, configuration, decodedEvent as any) const root = getRootPath(machine, completed.configuration) @@ -2233,16 +2132,16 @@ const macrostepConfiguration = < emittedEvents: [], microsteps: [], done: true as const, - output: completed.configuration.outputs.get(root) as Output + output: completed.configuration.outputs.get(root) as any } } const selections = InvocationEvent.isInvocationEvent(decodedEvent) - ? selectInvocationTransition(machine, configuration, decodedEvent) - : selectEventTransitions( + ? selectInvocationTransition(machine, configuration, decodedEvent) + : selectEventTransitions( machine, configuration, - decodedEvent as Machine.EventByTag> + decodedEvent ) if (selections.length === 0) { return { @@ -2303,40 +2202,26 @@ const snapshotMacrostep = < : { ...planned, done: false, output: undefined } } -const macrostep = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - state: Machine.Snapshot, - event: Machine.EventOf +const macrostep = ( + machine: Machine.Any, + state: Machine.Snapshot, + event: Machine.EventOf ) => { - const configuration = normalizeConfigurationSync(machine, state) + const configuration = normalizeConfigurationSync(machine, state) const settled = macrostepConfiguration(machine, configuration, event) - return snapshotMacrostep, E, R, Output>(machine, settled) + return snapshotMacrostep(machine, settled) } export const planSync = macrostep export const planConfiguration = macrostepConfiguration -const planningEffect = (thunk: () => A): Effect.Effect => +const planningEffect = (thunk: () => A): Effect.Effect => Effect.suspend(() => { try { return Effect.succeed(thunk()) } catch (error) { - return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError - ? Effect.fail(error) - : Effect.die(error) + return failPlanning(error) } }) @@ -2349,25 +2234,34 @@ const schemaEffect = (thunk: () => A): Effect.Effect, unknown]) => { +export function can( + machine: Machine.Any +): (state: Machine.Snapshot, event: unknown) => Effect.Effect +export function can( + machine: Machine.Any, + state: Machine.Snapshot, + event: unknown +): Effect.Effect +export function can( + ...args: readonly [Machine.Any] | readonly [Machine.Any, Machine.Snapshot, unknown] +): + | ((state: Machine.Snapshot, event: unknown) => Effect.Effect) + | Effect.Effect +{ const query = (state: Machine.Snapshot, event: unknown) => schemaEffect(() => canSync(args[0], state, event)) return args.length === 1 ? query : query(args[1], args[2]) } export const plan = (machine: Machine.Any, state: Machine.Snapshot, event: unknown) => - planningEffect(() => planSync(machine as any, state, event as any)) + planningEffect(() => planSync(machine, state, event)) export const planInitial = ( machine: Machine.Any, ...args: ReadonlyArray -): Effect.Effect => +): Effect.Effect, PlanningError | StartupError> => Effect.try({ - try: () => (planInitialSync as any)(machine, ...args), - catch: (error) => { - return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError - ? error - : new StartupError({ cause: Cause.die(error) }) - } + try: () => planInitialSync(machine, ...args), + catch: toStartupFailure }) // Captured constructors are private planner inputs. Public callbacks receive root data diff --git a/packages/effect-machine/src/internal/machine/process.ts b/packages/effect-machine/src/internal/machine/process.ts index 47c17eab..35536090 100644 --- a/packages/effect-machine/src/internal/machine/process.ts +++ b/packages/effect-machine/src/internal/machine/process.ts @@ -4,32 +4,28 @@ * @since 0.4.0 */ -import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Option from "effect/Option" -import type * as Schema from "effect/Schema" -import type { ActionError, ExecutionServices, Machine, Runtime } from "../../Machine.js" +import type { Machine, Runtime } from "../../Machine.js" import * as CommandRuntime from "./commandRuntime.js" import * as Configuration from "./configuration.js" -import { InfiniteTransitionError, MachineSchemaDecodeError, StartupError } from "./errors.js" -import type { StoppedError } from "./errors.js" +import { failPlanning, toStartupFailure } from "./errors.js" import * as ExecutionPlan from "./executionPlan.js" import { type CapturedStateConfig, toImpl } from "./implementation.js" import * as Invocation from "./invocation.js" import * as internalPlanner from "./planner.js" -import type { ExcludeCompatibleRuntime } from "./requirements.js" import * as internalRuntime from "./runtime.js" import * as internalRuntimeProtocol from "./runtimeProtocol.js" import * as Serialization from "./serialization.js" -type ProcessEntry = +type ProcessEntry = | { readonly _tag: "Initial" - readonly args: [...Machine.InputArgs] + readonly args: ReadonlyArray } | { readonly _tag: "Resume" - readonly snapshot: Machine.Snapshot + readonly snapshot: Machine.Snapshot } const runSequentialDiscard = ( @@ -129,9 +125,7 @@ const makeChildlessCompiledDrain = ( context.scope ) } catch (error) { - return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError - ? Effect.fail(error) - : Effect.die(error) + return failPlanning(error) } configuration = planned.next context.executionState = configuration @@ -254,9 +248,7 @@ const makeInvokingCompiledDrain = ( scope ) } catch (error) { - return error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError - ? Effect.fail(error) - : Effect.die(error) + return failPlanning(error) } configuration = planned.next if (planned.microsteps.length === 0) { @@ -370,69 +362,31 @@ const makeInvokingCompiledDrain = ( } } -const makeProcessLogic: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - entry: ProcessEntry -) => internalRuntimeProtocol.ProcessLogic< - Machine.Snapshot, - Machine.EventOf, - E | ActionError | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - >, - Output, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError -> = < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - entry: ProcessEntry -) => { +// Planned initial states only carry an output once the machine is done. +const initialResult = ( + planned: { readonly state: Machine.Snapshot; readonly done: boolean; readonly output: unknown } +): internalRuntimeProtocol.CompiledProcessInitial, unknown> => + planned.done + ? { state: planned.state, done: true, output: planned.output } + : { state: planned.state, done: false, output: undefined } + +// Process logic executes erased machine definitions. The public `Machine` +// module owns the typed contract that specializes this boundary. +const makeProcessLogic = ( + machine: Machine.Any, + entry: ProcessEntry +): internalRuntimeProtocol.ProcessLogic => { const hasInvokes = hasInvokeCapability(machine) const executionPlan = ExecutionPlan.compileExecutionPlan(machine) const initialArgs = entry._tag === "Initial" ? entry.args : [] const compiledInitial = entry._tag === "Initial" ? executionPlan.initial : undefined const makeCompiledInitial = compiledInitial === undefined ? undefined : ( - scope: internalRuntimeProtocol.ProcessScope> + scope: internalRuntimeProtocol.ProcessScope> ) => { try { const planned = compiledInitial(initialArgs, scope) scope.inspectInitial(planned.initialEntryPaths) - const result = { - state: planned.state as Machine.Snapshot, - done: planned.done, - output: planned.output as Output | undefined - } + const result = initialResult(planned) return hasInvokes ? { ...result, @@ -444,13 +398,11 @@ const makeProcessLogic: < } : result } catch (error) { - throw error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError - ? error - : new StartupError({ cause: Cause.die(error) }) + throw toStartupFailure(error) } } const makeInitial = ( - scope: internalRuntimeProtocol.ProcessScope> + scope: internalRuntimeProtocol.ProcessScope> ) => compiledInitial === undefined ? internalRuntimeProtocol.provideMachineRuntime( @@ -464,13 +416,9 @@ const makeProcessLogic: < ? undefined : CommandRuntime.runEmittedEvents( planned.emittedEvents, - CommandRuntime.makeLiveRuntime, Machine.EmittedEventOf>(machine, scope) + CommandRuntime.makeLiveRuntime(machine, scope) ) - const result = Effect.succeed({ - state: planned.state, - done: planned.done, - output: planned.output - }) + const result = Effect.succeed(initialResult(planned)) return commands === undefined ? emitted === undefined ? result : emitted.pipe(Effect.andThen(result)) : emitted === undefined @@ -480,14 +428,14 @@ const makeProcessLogic: < ), scope ) - : Effect.try({ try: () => makeCompiledInitial!(scope), catch: (error) => error as any }) + : Effect.try({ try: () => makeCompiledInitial!(scope), catch: toStartupFailure }) return ({ inspection: { kind: "Machine", definition: machine }, execution: { _tag: "Compiled", childless: !hasInvokes, - initial: entry._tag === "Initial" ? makeInitial : undefined, - initialSync: makeCompiledInitial, + ...(entry._tag === "Initial" ? { initial: makeInitial } : {}), + ...(makeCompiledInitial === undefined ? {} : { initialSync: makeCompiledInitial }), drain: { _tag: "Owned", run: hasInvokes @@ -509,11 +457,11 @@ const makeProcessLogic: < if (completeMessage === undefined || pollMessage === undefined || receiveMessage === undefined) { return yield* Effect.die(new Error("Machine statechart started without acknowledged mailbox access")) } - let terminal: { readonly output: Output } | undefined + let terminal: { readonly output: any } | undefined let current = yield* state if (internalPlanner.isFinalState(machine, current)) { - return yield* internalPlanner.getFinalOutputEffect( + return yield* internalPlanner.getFinalOutputEffect( machine, current, internalPlanner.InitialEvent @@ -528,9 +476,9 @@ const makeProcessLogic: < // per iteration; every iteration still crosses Effect boundaries, // so the Effect scheduler remains responsible for cooperative yield. let configuration: Configuration.ActiveConfiguration | undefined - let pendingMessage: Option.Option>> = Option + let pendingMessage: Option.Option>> = Option .none() - let liveRuntime: Runtime, Machine.EmittedEventOf> | undefined + let liveRuntime: Runtime, Machine.EmittedEventOf> | undefined while (terminal === undefined) { const message = Option.isSome(pendingMessage) ? pendingMessage.value : yield* receiveMessage pendingMessage = Option.none() @@ -548,21 +496,18 @@ const makeProcessLogic: < event ) } catch (error) { - if (error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError) { - return yield* error - } - throw error + return yield* failPlanning(error) } configuration = planned.next if (planned.microsteps.length > 0) { - const next = Configuration.snapshotFromConfiguration(machine, planned.next) + const next = Configuration.snapshotFromConfiguration(machine, planned.next) yield* CommandRuntime.runCommands(planned.commands, context) yield* setState(next) current = next if (planned.emittedEvents.length > 0) { yield* CommandRuntime.runEmittedEvents( - planned.emittedEvents as ReadonlyArray>, + planned.emittedEvents, liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, context) ) } @@ -610,16 +555,15 @@ const makeProcessLogic: < const startInvokes: ( configuration: Configuration.ActiveConfiguration, paths: ReadonlyArray, - event: Machine.LifecycleEvent - ) => Effect.Effect = (configuration, paths, event) => - (Invocation.startAll( - machine, - context, - ownedChildren, - configuration, - paths, - event - ) ?? Effect.void) as Effect.Effect + event: Machine.LifecycleEvent + ) => Effect.Effect = (configuration, paths, event) => (Invocation.startAll( + machine, + context, + ownedChildren, + configuration, + paths, + event + ) ?? Effect.void) const stopInvokes = (paths: ReadonlyArray): Effect.Effect => ownedChildren.stopPaths(paths) ?? Effect.void @@ -637,9 +581,9 @@ const makeProcessLogic: < // As above, keep the normalized configuration only while this // worker can continue draining an already queued batch. configuration = undefined - let pendingMessage: Option.Option>> = Option + let pendingMessage: Option.Option>> = Option .none() - let liveRuntime: Runtime, Machine.EmittedEventOf> | undefined + let liveRuntime: Runtime, Machine.EmittedEventOf> | undefined // Match the compact non-invoke loop while retaining state-scoped // child lifecycle work at the same ordered Effect boundaries. @@ -660,25 +604,22 @@ const makeProcessLogic: < event ) } catch (error) { - if (error instanceof InfiniteTransitionError || error instanceof MachineSchemaDecodeError) { - return yield* error - } - throw error + return yield* failPlanning(error) } configuration = planned.next if (planned.microsteps.length > 0) { const changed = planned.microsteps.some((step) => step.changed) const exitPaths = planned.microsteps.flatMap((step) => step.exitPaths) - const entryEvents = new Map>() + const entryEvents = new Map>() for (const step of planned.microsteps) { if (step.changed) { for (const path of step.entryPaths) { - entryEvents.set(path, step.event as Machine.LifecycleEvent) + entryEvents.set(path, step.event) } } } - const next = Configuration.snapshotFromConfiguration(machine, planned.next) + const next = Configuration.snapshotFromConfiguration(machine, planned.next) yield* CommandRuntime.runCommands(planned.commands, context) if (changed) { yield* stopInvokes(exitPaths) @@ -687,7 +628,7 @@ const makeProcessLogic: < current = next if (planned.emittedEvents.length > 0) { yield* CommandRuntime.runEmittedEvents( - planned.emittedEvents as ReadonlyArray>, + planned.emittedEvents, liveRuntime ??= CommandRuntime.makeLiveRuntime(machine, context) ) } @@ -734,24 +675,7 @@ const makeProcessLogic: < }), context ) - }) as internalRuntimeProtocol.ProcessLogic< - Machine.Snapshot, - Machine.EventOf, - E | ActionError | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - >, - Output, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError - > + }) } const initialProcessLogicCache = new WeakMap< @@ -759,39 +683,10 @@ const initialProcessLogicCache = new WeakMap< internalRuntimeProtocol.ProcessLogic >() -export const toProcessLogic: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - ...args: [...Machine.InputArgs] -) => internalRuntimeProtocol.ProcessLogic< - Machine.Snapshot, - Machine.EventOf, - E | ActionError | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - >, - Output, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError -> = (machine, ...args) => { +export const toProcessLogic = ( + machine: Machine.Any, + ...args: ReadonlyArray +): internalRuntimeProtocol.ProcessLogic => { if (args.length > 0) { return makeProcessLogic(machine, { _tag: "Initial", args }) } @@ -813,7 +708,7 @@ const toResumedProcessLogic = ( machine: Machine.Any, snapshot: Machine.Snapshot ): internalRuntimeProtocol.ProcessLogic => - (makeProcessLogic as any)(machine, { _tag: "Resume", snapshot }) + makeProcessLogic(machine, { _tag: "Resume", snapshot }) /** @internal Test-only runtime strategy selection for a fresh machine. */ export const startWithRuntimeStrategyForTesting = ( @@ -822,7 +717,7 @@ export const startWithRuntimeStrategyForTesting = ( ...args: ReadonlyArray ): Effect.Effect, any, any> => internalRuntime.startProcessWithStrategyForTesting( - (toProcessLogic as any)(machine, ...args), + toProcessLogic(machine, ...args), strategy, machine.id === undefined ? undefined : { id: machine.id } ) @@ -834,7 +729,7 @@ export const prepareWithRuntimeStrategyForTesting = ( ...args: ReadonlyArray ): Effect.Effect, any, any> => internalRuntime.prepareProcessWithStrategyForTesting( - (toProcessLogic as any)(machine, ...args), + toProcessLogic(machine, ...args), strategy, machine.id === undefined ? undefined : { id: machine.id } ) @@ -851,125 +746,31 @@ export const resumeWithRuntimeStrategyForTesting = ( machine.id === undefined ? undefined : { id: machine.id } ) -export const start: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - ...args: [...Machine.InputArgs] -) => Effect.Effect< - internalRuntimeProtocol.MachineRef< - Machine.Snapshot, - Machine.EventOf, - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StoppedError, - Output - >, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - > -> = (machine, ...args) => +// The public `Machine` module owns the typed start, prepare, and resume +// signatures. These implementations are the erased boundary they specialize. +export const start = ( + machine: Machine.Any, + ...args: ReadonlyArray +): Effect.Effect, any, any> => internalRuntime.startProcess( toProcessLogic(machine, ...args), machine.id === undefined ? undefined : { id: machine.id } - ) as any - -export const prepare: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - ...args: [...Machine.InputArgs] -) => Effect.Effect< - internalRuntimeProtocol.PreparedProcess< - Machine.Snapshot, - Machine.EventOf, - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StoppedError, - Output, - Machine.EmittedEventOf, - | InitialE - | E - | ActionError - | InfiniteTransitionError - | MachineSchemaDecodeError - | StartupError - | StoppedError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - > - > -> = (machine, ...args) => + ) + +export const prepare = ( + machine: Machine.Any, + ...args: ReadonlyArray +): Effect.Effect> => internalRuntime.prepareProcess( toProcessLogic(machine, ...args), machine.id === undefined ? undefined : { id: machine.id } - ) as any - -export const resume: < - const States extends Machine.StateSchemas, - const Events extends ReadonlyArray, - const Emits extends ReadonlyArray = readonly [], - const Input extends Schema.Top = typeof Schema.Void, - UnhandledStates extends Machine.StateIdentifier = Machine.StateIdentifier, - E = never, - R = never, - InitialE = never, - InitialR = never, - FinalStates extends Machine.StateIdentifier = never, - Output = never ->( - machine: Machine, - snapshot: Machine.Snapshot -) => Effect.Effect< - internalRuntimeProtocol.MachineRef< - Machine.Snapshot, - Machine.EventOf, - E | ActionError | InfiniteTransitionError | MachineSchemaDecodeError | StoppedError, - Output - >, - MachineSchemaDecodeError, - ExcludeCompatibleRuntime< - Exclude, internalRuntimeProtocol.MachineRuntime>, - Machine.EventOf, - Machine.EmittedEventOf - > -> = (machine, snapshot) => + ) + +export const resume = ( + machine: Machine.Any, + snapshot: Machine.Snapshot +): Effect.Effect, any, any> => internalRuntime.startProcess( toResumedProcessLogic(machine, snapshot), machine.id === undefined ? undefined : { id: machine.id } - ) as any + ) diff --git a/packages/effect-machine/src/internal/machine/targetReference.ts b/packages/effect-machine/src/internal/machine/targetReference.ts index 4e2f68df..5a2bbcc0 100644 --- a/packages/effect-machine/src/internal/machine/targetReference.ts +++ b/packages/effect-machine/src/internal/machine/targetReference.ts @@ -1,19 +1,19 @@ import * as Schema from "effect/Schema" -import type { Machine, State } from "../../Machine.js" +import type { Machine } from "../../Machine.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 root: Machine.Any["root"] 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 } => { +export const make = (root: Machine.Any["root"]): { readonly root: Reference } => { const nodes: Array<{ path: string; key: string; parent: string | undefined; type: string }> = [] const visit = ( node: Machine.StateNodeConfig | Machine.TaggedSchema, diff --git a/packages/effect-machine/test/internal/machine/planningFailures.test.ts b/packages/effect-machine/test/internal/machine/planningFailures.test.ts new file mode 100644 index 00000000..6bd71240 --- /dev/null +++ b/packages/effect-machine/test/internal/machine/planningFailures.test.ts @@ -0,0 +1,122 @@ +import { assert, describe, it } from "@effect/vitest" +import { Cause, Effect, Exit, Schema } from "effect" +import { Machine } from "../../../src/index.js" +import * as ExecutionPlan from "../../../src/internal/machine/executionPlan.js" +import { startWithRuntimeStrategyForTesting } from "../../../src/internal/machine/process.js" + +class Idle extends Schema.TaggedClass("PlanningFailureIdle")("Idle", {}) {} +class Busy extends Schema.TaggedClass("PlanningFailureBusy")("Busy", {}) {} +class Waiting extends Schema.TaggedClass("PlanningFailureWaiting")("Waiting", {}) {} +class Go extends Schema.TaggedClass("PlanningFailureGo")("Go", {}) {} +class Loop extends Schema.TaggedClass("PlanningFailureLoop")("Loop", {}) {} + +const defect = new Error("handler defect") + +// Uses only indexed-planner capabilities so the optimized initial and drain +// paths are exercised alongside the generic reference. +const makeDefectMachine = (options: { readonly failInitial: boolean }) => { + const root = Machine.state({ states: { Idle, Busy } }) + const targets = Machine.targets(root) + return Machine.make({ + root, + events: Machine.eventsFromSchemas(Go) + }).handle({ + initial: { + target: targets.root.Idle, + decoded: true, + data: () => { + if (options.failInitial) throw defect + return new Idle() + } + }, + states: { + Idle: { + on: { + Go: { + target: targets.root.Busy, + decoded: true, + data: () => { + throw defect + } + } + } + }, + Busy: {} + } + }) +} + +// Idle and Busy route to each other through `always`, so planning never +// stabilizes once either is entered. +const makeLoopMachine = (options: { readonly loopInitial: boolean }) => { + const root = Machine.state({ states: { Waiting, Idle, Busy } }) + const targets = Machine.targets(root) + const loop = { + Idle: { always: { target: targets.root.Busy, decoded: true, data: () => new Busy() } }, + Busy: { always: { target: targets.root.Idle, decoded: true, data: () => new Idle() } } + } as const + const machine = Machine.make({ + root, + events: Machine.eventsFromSchemas(Loop) + }) + return options.loopInitial + ? machine.handle({ + initial: { target: targets.root.Idle, decoded: true, data: new Idle() }, + states: { Waiting: {}, ...loop } + }) + : machine.handle({ + initial: { target: targets.root.Waiting, decoded: true, data: new Waiting() }, + states: { + Waiting: { on: { Loop: { target: targets.root.Idle, decoded: true, data: () => new Idle() } } }, + ...loop + } + }) +} + +describe("planning failure classification", () => { + it("compiles the defect fixture to an optimized planner", () => { + assert.notStrictEqual( + ExecutionPlan.selectExecutionPlanForTesting(makeDefectMachine({ failInitial: false }), "auto").strategy, + "generic" + ) + }) + + for (const strategy of ["generic", "compiled"] as const) { + it.effect(`${strategy}: wraps an initializer defect in StartupError`, () => + Effect.gen(function*() { + const error = yield* Effect.flip( + startWithRuntimeStrategyForTesting(makeDefectMachine({ failInitial: true }), strategy) + ) + assert.instanceOf(error, Machine.StartupError) + assert.strictEqual(Cause.squash(error.cause), defect) + })) + + it.effect(`${strategy}: reports a transition handler throw as a defect`, () => + Effect.gen(function*() { + const ref = yield* startWithRuntimeStrategyForTesting(makeDefectMachine({ failInitial: false }), strategy) + yield* ref.send(new Go()) + const exit = yield* Effect.exit(ref.join) + assert(Exit.isFailure(exit)) + assert(Cause.hasDies(exit.cause)) + assert.strictEqual(Cause.squash(exit.cause), defect) + })) + + it.effect(`${strategy}: keeps initial non-stabilization typed`, () => + Effect.gen(function*() { + const error = yield* Effect.flip( + startWithRuntimeStrategyForTesting(makeLoopMachine({ loopInitial: true }), strategy) + ) + assert.instanceOf(error, Machine.InfiniteTransitionError) + })) + + it.effect(`${strategy}: keeps event-time non-stabilization typed`, () => + Effect.gen(function*() { + const ref = yield* startWithRuntimeStrategyForTesting(makeLoopMachine({ loopInitial: false }), strategy) + yield* ref.send(new Loop()) + const exit = yield* Effect.exit(ref.join) + assert(Exit.isFailure(exit)) + assert.isFalse(Cause.hasDies(exit.cause)) + assert.instanceOf(Cause.squash(exit.cause), Machine.InfiniteTransitionError) + })) + } +})