From ac855f352f6ec9c54591f12ae3ddf3905cc4dd89 Mon Sep 17 00:00:00 2001 From: SandroMaglione Date: Sun, 1 Nov 2026 16:40:16 +0900 Subject: [PATCH] Type planner target resolution and runtime child operations Build resolved history and initial targets through one erased constructor, type choice routes, let validating normalizers accept unknown input, and expose the transition event through the evaluated context's type. Replace the overloaded internal spawn, sendTo, and stopChild with erased implementations checked by the public Machine signatures, and type invocation sources as event-agnostic process logic. Co-Authored-By: Claude Opus 5.5 --- .changeset/typed-planner-transitions.md | 5 + .../src/internal/machine/configuration.ts | 9 +- .../src/internal/machine/invocation.ts | 38 ++---- .../src/internal/machine/machine.ts | 93 +++---------- .../src/internal/machine/planner.ts | 123 +++++++----------- .../src/internal/machine/topology.ts | 22 ++++ .../internal/testing/machine/finiteModel.ts | 4 +- .../machine/ProtocolValidation.test.ts | 2 +- .../machine/strategyDifferential.test.ts | 2 +- 9 files changed, 114 insertions(+), 184 deletions(-) create mode 100644 .changeset/typed-planner-transitions.md diff --git a/.changeset/typed-planner-transitions.md b/.changeset/typed-planner-transitions.md new file mode 100644 index 0000000..e641568 --- /dev/null +++ b/.changeset/typed-planner-transitions.md @@ -0,0 +1,5 @@ +--- +"@typeonce/effect-machine": patch +--- + +Internal: check planner target resolution, invocation sources, and runtime child operations at compile time instead of through `any` casts. Machine behavior and the public API are unchanged. diff --git a/packages/effect-machine/src/internal/machine/configuration.ts b/packages/effect-machine/src/internal/machine/configuration.ts index 3f6da38..d77d706 100644 --- a/packages/effect-machine/src/internal/machine/configuration.ts +++ b/packages/effect-machine/src/internal/machine/configuration.ts @@ -577,9 +577,9 @@ export const normalizeConfiguration = ): ActiveConfiguration => configurationFromSnapshot(machine, state) -export const normalizeConfigurationSync = ( +export const normalizeConfigurationSync = ( machine: Machine.Any, - state: Machine.Snapshot + state: Machine.AtomicSnapshot ): ActiveConfiguration => { try { return configurationFromSnapshot(machine, state) @@ -1095,10 +1095,11 @@ const configurationFromTargetSnapshotSync = ( } } -export const normalizeTargetConfigurationSync = ( +/** Applies a transition target, failing for anything other than a target builder result or snapshot. */ +export const normalizeTargetConfigurationSync = ( machine: Machine.Any, current: ActiveConfiguration, - target: Machine.Snapshot | Machine.Target> + target: unknown ): ActiveConfiguration => { if (isTarget(target)) { const snapshot = target[TargetSnapshotTypeId] diff --git a/packages/effect-machine/src/internal/machine/invocation.ts b/packages/effect-machine/src/internal/machine/invocation.ts index b9cea39..19ca664 100644 --- a/packages/effect-machine/src/internal/machine/invocation.ts +++ b/packages/effect-machine/src/internal/machine/invocation.ts @@ -7,7 +7,7 @@ import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Stream from "effect/Stream" -import type { ChildMachine, ChildOwner, Inspection, Logic, Machine } from "../../Machine.js" +import type { ChildMachine, ChildOwner, Inspection, Machine } from "../../Machine.js" import * as ChildRegistry from "./childRegistry.js" import * as Configuration from "./configuration.js" import { ChildAlreadyExistsError, InfiniteTransitionError, MachineSchemaDecodeError, StoppedError } from "./errors.js" @@ -37,7 +37,9 @@ export const makeKey = (path: string, id: string): string => `${path.length}:${p export const makeChildId = (path: string, id: string): string => `${ChildRegistry.invocationChildIdPrefix}${makeKey(path, id)}` -const oneShot = (effect: Effect.Effect): Logic => ({ +const oneShot = ( + effect: Effect.Effect +): Runtime.ProcessLogic => ({ initial: () => Effect.void, run: () => effect }) @@ -46,7 +48,7 @@ const streamLogic = ( stream: Stream.Stream, path: string, id: string -): Runtime.ProcessLogic => ({ +): Runtime.ProcessLogic => ({ initial: () => Effect.void, run: ({ parent }) => { const send = parent?.[Runtime.acknowledgedSend] @@ -68,9 +70,10 @@ const resolveValue = (value: unknown, context: Omit): ChildOwner => ({ - spawn: - ((descriptor: ChildMachine.Any, options?: { readonly input?: unknown }) => - (scope.spawn as any)(descriptor, options)) as ChildOwner["spawn"], + spawn: ( + child: Child & ChildMachine.Executable & ChildMachine.ParentCompatibility, + ...options: ChildMachine.SpawnArgs + ) => scope.spawn(child, ...options), sendTo: ((descriptor: ChildMachine.Any, event: unknown) => scope.sendTo(descriptor, event)) as ChildOwner< any >["sendTo"], @@ -86,15 +89,7 @@ const resolveOne = ( return { id: String(raw.id), sourceName: raw.sourceName, - src: () => - oneShot(raw.effect(context) as Effect.Effect) as unknown as Runtime.ProcessLogic< - any, - any, - any, - any, - any, - any - >, + src: () => oneShot(raw.effect(context)), onDone: raw.onDone, onFailure: raw.onFailure, onSnapshot: raw.onSnapshot, @@ -105,15 +100,7 @@ const resolveOne = ( return { id: String(raw.id), sourceName: raw.sourceName, - src: () => - oneShot(Effect.sleep(resolveValue(raw.after, context) as any)) as unknown as Runtime.ProcessLogic< - any, - any, - any, - any, - any, - any - >, + src: () => oneShot(Effect.sleep(typeof raw.after === "function" ? raw.after(context) : raw.after)), onDone: raw.onDone, activityKind: "Timer" } @@ -123,8 +110,7 @@ const resolveOne = ( return { id, sourceName: raw.sourceName, - src: () => - streamLogic(raw.stream(context), path, id) as unknown as Runtime.ProcessLogic, + src: () => streamLogic(raw.stream(context), path, id), onDone: raw.onDone, onFailure: raw.onFailure, activityKind: "Stream" diff --git a/packages/effect-machine/src/internal/machine/machine.ts b/packages/effect-machine/src/internal/machine/machine.ts index b6a6d73..6f5f833 100644 --- a/packages/effect-machine/src/internal/machine/machine.ts +++ b/packages/effect-machine/src/internal/machine/machine.ts @@ -4,7 +4,6 @@ import * as Option from "effect/Option" import { Prototype as PipeablePrototype } from "effect/Pipeable" import { hasProperty } from "effect/Predicate" import * as Schema from "effect/Schema" -import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" import type { ChildAddress, @@ -26,7 +25,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, PlanningError, StartupError } from "./errors.js" +import type { PlanningError, StartupError } from "./errors.js" import type { CapturedStateConfig } from "./implementation.js" import * as InitialDeclaration from "./initialDeclaration.js" import * as InvocationDefinition from "./invocationDefinition.js" @@ -59,18 +58,6 @@ export const InvokeTypeId: unique symbol = Symbol.for("effect/Machine/Invoke") const ChildMachineTypeId = "~effect/Machine/ChildMachine" type MachineRuntimeRequirement = internalRuntime.MachineRuntime -type SpawnRequirements = Exclude -type SpawnIdError = "id" extends keyof Options ? Options extends { - readonly id?: infer Id - } ? [Id] extends [undefined] ? never : ChildAlreadyExistsError - : ChildAlreadyExistsError - : never -type SpawnError = SpawnIdError -type SpawnResult = Effect.Effect< - MachineRef, - SpawnError | InitialError, - MachineRuntimeRequirement | SpawnRequirements -> const Proto = { ...Inspectable.BaseProto, ...PipeablePrototype, @@ -829,7 +816,7 @@ const makeStateHelpers = (): Machine.StateAccessors<{ readonly "": Machine.State Option.map((snapshot) => ({ value: snapshot.value, parents })) ) }) as Machine.StateAccessors<{ readonly "": Machine.StateNodeConfig }>["getWithParents"], - getSnapshot: Topology.getSnapshotByPath as unknown as Machine.StateAccessors< + getSnapshot: Topology.getSnapshotByPath as Machine.StateAccessors< { readonly "": Machine.StateNodeConfig } >["getSnapshot"], matches: @@ -965,6 +952,8 @@ export const planInitial: ( ...args: ReadonlyArray ) => Effect.Effect = internalPlanner.planInitial +// Erased boundary: compiled nodes are stored with string paths; the generic +// machine type is the only source of their literal identifiers. export const stateNodes = ( machine: M ): ReadonlyArray< @@ -1122,73 +1111,31 @@ export const childFamily = (machine: M): ChildMachine.Fam export const childAddress = (id: string): ChildAddress => id as ChildAddress -export const spawn: { - ( - logic: Logic< - ChildState, - ChildEvent, - ChildError, - ChildRequirements, - ChildOutput, - ChildInitialError - > - ): SpawnResult - < - ChildState, - ChildEvent, - ChildError, - ChildRequirements, - ChildOutput, - Options extends SpawnOptions, - ChildInitialError = never - >( - logic: Logic< - ChildState, - ChildEvent, - ChildError, - ChildRequirements, - ChildOutput, - ChildInitialError - >, - options: Options & ChildAddress.OptionsCompatibility - ): SpawnResult< - ChildState, - ChildEvent, - ChildError, - ChildRequirements, - ChildOutput, - SpawnError, - ChildInitialError - > -} = ((logic: Logic, options?: SpawnOptions) => +// The public `Machine` module declares the typed overloads and checks these +// erased implementations against them structurally. +export const spawn = (logic: Logic, options?: SpawnOptions) => Effect.flatMap( internalRuntime.MachineRuntime, - (runtime) => options === undefined ? runtime.spawn(logic) : (runtime.spawn as any)(logic, options) - )) as any - -export const sendTo: { - ( - child: Child, - event: ChildMachine.Event - ): Effect.Effect -
>( - id: Address, - event: ChildAddress.Event
- ): Effect.Effect -} = ((child: string | ChildMachine.Any, event: unknown) => + // `SpawnOptions` declares only `id`; an absent id spawns an anonymous child. + (runtime) => options?.id === undefined ? runtime.spawn(logic) : runtime.spawn(logic, { id: options.id }) + ) + +export const sendTo = ( + child: string | ChildMachine.Any, + event: unknown +): Effect.Effect => Effect.flatMap( internalRuntime.MachineRuntime, (runtime) => runtime.sendTo(child, event) - )) as any + ) -export const stopChild: { - (child: ChildAddress): Effect.Effect - (child: Child): Effect.Effect -} = ((child: string | ChildMachine.Any) => +export const stopChild = ( + child: string | ChildMachine.Any +): Effect.Effect => Effect.flatMap( internalRuntime.MachineRuntime, (runtime) => runtime.stopChild(child) - )) as any + ) export const watch = ( ref: MachineRef diff --git a/packages/effect-machine/src/internal/machine/planner.ts b/packages/effect-machine/src/internal/machine/planner.ts index 6bf0636..c51627b 100644 --- a/packages/effect-machine/src/internal/machine/planner.ts +++ b/packages/effect-machine/src/internal/machine/planner.ts @@ -48,6 +48,7 @@ import { decodeEventSync, decodeInputSync, decodeStateValueSync } from "./protoc import { InitialEventTypeId } from "./symbols.js" import { getNode, + type HistoryTarget, type InitialTarget as InitialTargetInstruction, isChoiceTarget, isCombinedTarget, @@ -59,6 +60,7 @@ import { isStateUpdate, isTarget, makeChoiceTarget, + makeResolvedTarget, makeStateInput, makeTarget, TargetSnapshotTypeId @@ -313,7 +315,7 @@ const completeHistoryConfiguration = ( next = normalizeTargetConfigurationSync( machine, next, - (resolved?.target ?? routed) as Machine.Snapshot | Machine.Target + resolved?.target ?? routed ) if (resolved !== undefined) { commands.push(...resolved.commands) @@ -460,7 +462,7 @@ export function resolveInitialConfiguration( partial = normalizeTargetConfigurationSync( machine, partial, - (resolved?.target ?? routed) as Machine.Target + resolved?.target ?? routed ) if (resolved !== undefined) { commands.push(...resolved.commands) @@ -494,10 +496,7 @@ export function resolveInitialTarget( const completed = resolveInitialConfiguration(machine, configuration, target, event) const snapshot = snapshotFromConfigurationAtPath(machine, completed.configuration, target.path) return { - target: makeTarget(target.path as any, snapshot.value as any, { - snapshot: snapshot as any, - values: target.values as any - }), + target: makeResolvedTarget(target.path, snapshot.value, { snapshot, values: target.values }), commands: completed.commands, raisedEvents: completed.raisedEvents, emittedEvents: completed.emittedEvents, @@ -526,10 +525,7 @@ function resolveHistoryTarget( const values = Object.fromEntries(completed.configuration.values) if (target.parent !== "" && configuration.active.has("")) delete values[""] return { - target: makeTarget(target.parent as any, snapshot.value as any, { - snapshot: snapshot as any, - values: values as any - }), + target: makeResolvedTarget(target.parent, snapshot.value, { snapshot, values }), commands: completed.commands, raisedEvents: completed.raisedEvents, emittedEvents: completed.emittedEvents, @@ -565,20 +561,18 @@ function resolveHistoryTarget( event ) let fallbackConfiguration = choiceResolution === undefined - ? normalizeConfigurationSync(machine, collected.state as any) + ? normalizeConfigurationSync(machine, collected.state) : normalizeTargetConfigurationSync(machine, { active: new Set(), values: new Map(), outputs: new Map(), history: configuration.history - }, choiceResolution.target as any) + }, choiceResolution.target) for (const additionalTarget of choiceResolution?.additionalTargets ?? []) { fallbackConfiguration = normalizeTargetConfigurationSync( machine, fallbackConfiguration, - additionalTarget as - | Machine.Snapshot - | Machine.Target + additionalTarget ) } if (!fallbackConfiguration.active.has(target.parent)) { @@ -590,10 +584,7 @@ function resolveHistoryTarget( const values = Object.fromEntries(fallbackConfiguration.values) if (target.parent !== "" && configuration.active.has("")) delete values[""] return { - target: makeTarget(target.parent as any, snapshot.value as any, { - snapshot: snapshot as any, - values: values as any - }), + target: makeResolvedTarget(target.parent, snapshot.value, { snapshot, values }), commands: [...collected.commands, ...(choiceResolution?.commands ?? [])], raisedEvents: [...collected.raisedEvents, ...(choiceResolution?.raisedEvents ?? [])], emittedEvents: [...collected.emittedEvents, ...(choiceResolution?.emittedEvents ?? [])], @@ -885,12 +876,7 @@ const selectAlwaysTransitions = < sourcePath: path, leafPath: leaf, trigger: { type: "always" }, - transition: always as unknown as MicrostepTransition< - States, - E, - R, - ConstructionContext>> - >, + transition: always, context: { ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier< @@ -961,12 +947,7 @@ const selectDoneTransitions = < sourcePath: completion.path, leafPath: getActiveLeafPathFrom(machine, configuration, completion.path), trigger: { type: "done" }, - transition: onDone as unknown as MicrostepTransition< - States, - E, - R, - ConstructionContext>> - >, + transition: onDone, context: makeDoneContext>( machine, configuration, @@ -1042,29 +1023,14 @@ const selectEventTransitions = < sourcePath: path, leafPath: leaf, trigger: { type: "event", event: event._tag }, - transition: transition as unknown as MicrostepTransition< - States, - E, - R, - ConstructionContext< - Machine.HandlerContext< - States, - Events, - Emits, - Machine.StateIdentifier, - Machine.TagOf, - E, - R - > - > - >, + transition, context: makeTransitionContext< States, Events, Emits, Machine.StateIdentifier, Machine.TagOf - >(machine as any, configuration, path, event, capturedSnapshot()) + >(machine, configuration, path, event, capturedSnapshot()) }) evaluatedSources.set(path, candidate) } @@ -1127,19 +1093,14 @@ const selectInvocationTransition = < sourcePath: event.path, leafPath: getActiveLeafPathFrom(machine, configuration, event.path), trigger: { type: "invoke", id: event.id, outcome: event.type }, - transition: transition as unknown as MicrostepTransition, + transition, context }) return candidate === undefined ? [] : [candidate] } -export const getTargetNodePath = ( - target: - | Machine.Snapshot - | Machine.Target> - | Machine.HistoryTarget> - | Machine.ChoiceTarget> -): string => { +/** Reads the state path addressed by a transition result, failing for any other value. */ +export const getTargetNodePath = (target: unknown): string => { if (isHistoryTarget(target) || isChoiceTarget(target)) { return String(target.path) } @@ -1304,6 +1265,12 @@ interface ResolvedChoiceTransition { readonly updates: ReadonlyArray } +/** A target resolved to concrete state paths, before configuration normalization. */ +type ResolvedTarget = Machine.Target | Machine.AtomicSnapshot + +/** A route selected by choice resolution; initial and history routes are resolved by the caller. */ +type ChoiceRoute = ResolvedTarget | HistoryTarget | InitialTargetInstruction + function resolveChoiceTarget( machine: Machine.Any, configuration: ActiveConfiguration, @@ -1311,7 +1278,7 @@ function resolveChoiceTarget( event: unknown ) { const pending: Array = [initialTarget] - const resolvedTargets: Array = [] + const resolvedTargets: Array = [] const commands: Array = [] const raisedEvents: Array = [] const emittedEvents: Array = [] @@ -1373,7 +1340,7 @@ function resolveChoiceTarget( throw new Error(`Machine choice resolver for "${node.path}" must return a target`) } validateDeclaredTransitionTarget(node.path, { type: "choice" }, choice.targets, collected.state) - const returnedPath = getTargetNodePath(collected.state as any) + const returnedPath = getTargetNodePath(collected.state) current = withChoiceValues(collected.state, extracted.values) const nested = choiceFromTarget(current) transitions.push({ @@ -1412,7 +1379,7 @@ const collectEvaluatedTransition = < Event, E, R, - Context + Context extends { readonly event?: unknown } >( machine: Machine.Any, state: ActiveConfiguration, @@ -1471,11 +1438,11 @@ const collectEvaluatedTransition = < machine, constructionState, unresolvedTarget, - (selection.context as any).event + selection.context.event ) const choiceResolvedTarget = choiceResolution?.target ?? unresolvedTarget const initialResolution = choiceResolvedTarget !== undefined && isInitialTarget(choiceResolvedTarget) - ? resolveInitialTarget(machine, constructionState, choiceResolvedTarget, (selection.context as any).event) + ? resolveInitialTarget(machine, constructionState, choiceResolvedTarget, selection.context.event) : undefined const routedTarget = initialResolution?.target ?? choiceResolvedTarget let historyResolution: { @@ -1509,7 +1476,7 @@ const collectEvaluatedTransition = < machine, stateAtHistoryResolution, routedTarget, - (selection.context as any).event + selection.context.event ) } const target: Machine.Snapshot | Machine.Target> | undefined = @@ -1525,18 +1492,16 @@ const collectEvaluatedTransition = < const additionalTargetRaisedEvents: Array = [] const additionalTargetEmittedEvents: Array = [] const additionalTargetChoiceTransitions: Array = [] - const additionalChoiceTargets: Array< - Machine.Snapshot | Machine.Target> - > = [] + const additionalChoiceTargets: Array = [] for (const additionalTarget of choiceResolution?.additionalTargets ?? []) { if (isInitialTarget(additionalTarget)) { const resolved = resolveInitialTarget( machine, state, additionalTarget, - (selection.context as any).event + selection.context.event ) - additionalChoiceTargets.push(resolved.target as any) + additionalChoiceTargets.push(resolved.target) additionalTargetActions.push(...resolved.commands) additionalTargetRaisedEvents.push(...resolved.raisedEvents) additionalTargetEmittedEvents.push(...resolved.emittedEvents) @@ -1544,16 +1509,16 @@ const collectEvaluatedTransition = < continue } if (!isHistoryTarget(additionalTarget)) { - additionalChoiceTargets.push(additionalTarget as any) + additionalChoiceTargets.push(additionalTarget) continue } const resolved = resolveHistoryTarget( machine, state, additionalTarget, - (selection.context as any).event + selection.context.event ) - additionalChoiceTargets.push(resolved.target as any) + additionalChoiceTargets.push(resolved.target) additionalTargetActions.push(...resolved.commands) additionalTargetRaisedEvents.push(...resolved.raisedEvents) additionalTargetEmittedEvents.push(...resolved.emittedEvents) @@ -1569,9 +1534,9 @@ const collectEvaluatedTransition = < ...state, values: new Map(state.values).set(update.path, update.value) } - : normalizeTargetConfigurationSync(machine, state, target) + : normalizeTargetConfigurationSync(machine, state, target) for (const additionalTarget of additionalChoiceTargets) { - stateAfterTransition = normalizeTargetConfigurationSync( + stateAfterTransition = normalizeTargetConfigurationSync( machine, stateAfterTransition, additionalTarget @@ -1838,7 +1803,7 @@ const canSync = ( const decodedEvent = decodeEventSync(machine, event) if (isFinalState(machine, state)) return false const configuration = normalizeConfigurationSync(machine, state) - return selectEventTransitions(machine, configuration, decodedEvent as any).length > 0 + return selectEventTransitions(machine, configuration, decodedEvent).length > 0 } const microstep = ( @@ -2041,7 +2006,7 @@ const settle = ( throw new Error("Machine reached a terminal configuration without a completed root output") } completedTerminal = true - finalOutput = currentState.outputs.get(root) as any + finalOutput = currentState.outputs.get(root) break } @@ -2104,7 +2069,7 @@ const settle = ( microsteps } return completedTerminal - ? { ...result, done: true as const, output: finalOutput as any } + ? { ...result, done: true as const, output: finalOutput } : { ...result, done: false as const, output: undefined } } @@ -2115,6 +2080,8 @@ const macrostepConfiguration = ( ) => { const decodedEvent = InvocationEvent.isInvocationEvent(event) ? event : decodeEventSync(machine, event) if (isActiveFinalConfiguration(machine, configuration)) { + // Completion contexts receive the triggering event, including private invocation events + // that the public lifecycle event type does not model. const completed = completeConfigurationSync(machine, configuration, decodedEvent as any) const root = getRootPath(machine, completed.configuration) if (!completed.configuration.outputs.has(root)) { @@ -2127,7 +2094,7 @@ const macrostepConfiguration = ( emittedEvents: [], microsteps: [], done: true as const, - output: completed.configuration.outputs.get(root) as any + output: completed.configuration.outputs.get(root) } } @@ -2152,7 +2119,7 @@ const macrostepConfiguration = ( const step = microstep( machine, configuration, - decodedEvent as any, + decodedEvent, selections ) const commands = [...step.commands] @@ -2160,7 +2127,7 @@ const macrostepConfiguration = ( const emittedEvents = [...step.emittedEvents] const microsteps = [step] return { - ...settle(machine, step.next, decodedEvent as any, commands, raisedEvents, emittedEvents, microsteps), + ...settle(machine, step.next, decodedEvent, commands, raisedEvents, emittedEvents, microsteps), event: decodedEvent } } diff --git a/packages/effect-machine/src/internal/machine/topology.ts b/packages/effect-machine/src/internal/machine/topology.ts index 997c74c..31f33bd 100644 --- a/packages/effect-machine/src/internal/machine/topology.ts +++ b/packages/effect-machine/src/internal/machine/topology.ts @@ -613,6 +613,28 @@ export const makeTarget = < values: options?.values }) as Machine.Target +/** + * Builds a target from a planned configuration whose path, value, snapshot, + * and ancestor values were already decoded, so no state schema type applies. + * It keeps the same own-property shape as `makeTarget`, including `values` + * when undefined, which the optional public `values` field cannot express. + */ +export const makeResolvedTarget = ( + path: string, + value: unknown, + options: { + readonly snapshot: Machine.AtomicSnapshot + readonly values: Readonly> | undefined + } +): Machine.Target => + ({ + [TargetTypeId]: TargetTypeId, + [TargetSnapshotTypeId]: options.snapshot, + path, + value, + values: options.values + }) as Machine.Target + export const isTarget = (u: unknown): u is Machine.Target => hasProperty(u, TargetTypeId) export const makeStateInput = (input: unknown): StateInput => ({ diff --git a/packages/effect-machine/src/internal/testing/machine/finiteModel.ts b/packages/effect-machine/src/internal/testing/machine/finiteModel.ts index ca24f43..e7f0d2a 100644 --- a/packages/effect-machine/src/internal/testing/machine/finiteModel.ts +++ b/packages/effect-machine/src/internal/testing/machine/finiteModel.ts @@ -1456,6 +1456,8 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => { const flattened = validateModel(model) const byPath = new Map(flattened.map((state) => [state.path, state])) const stateTree = makeStateTree(model.roots, undefined) + // Erased boundary: the public constructors infer literal state and event types, + // but a finite model builds its schema tree and handlers at runtime. const defined = Machine.state({ states: stateTree } as any) const eventSchemas = model.events.map((event) => Schema.TaggedStruct(event, {})) const initial = byPath.get(model.initial)! @@ -1468,7 +1470,7 @@ export const compileModel = (model: FiniteModel): Machine.Machine.Any => { const machine = Machine.make({ root: defined, branches, - events: Machine.eventsFromSchemas(...eventSchemas) as any + events: Machine.eventsFromSchemas(...eventSchemas) } as any) return machine.handle( { diff --git a/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts b/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts index e859429..6872357 100644 --- a/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts +++ b/packages/effect-machine/test/internal/machine/ProtocolValidation.test.ts @@ -37,7 +37,7 @@ describe("event validation across execution strategies", () => { const selected = ExecutionPlan.selectExecutionPlanForTesting(machine, strategy) assert.strictEqual(selected.strategy, strategy === "auto" ? "indexed-flat" : "generic") const state = selected.plan.fromConfiguration( - Configuration.normalizeConfigurationSync>(machine, initial.state) + Configuration.normalizeConfigurationSync(machine, initial.state) ) selected.plan.plan(state, events.Set({ value: 1 }), true) Object.assign(retained as object, { value: "invalid" }) diff --git a/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts b/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts index bdc6785..d778521 100644 --- a/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts +++ b/packages/effect-machine/test/internal/machine/strategyDifferential.test.ts @@ -397,7 +397,7 @@ describe("machine planner and runtime strategies", () => { const machine = makeFlatMachine() const initial = yield* Machine.planInitial(machine) const selected = ExecutionPlan.selectExecutionPlanForTesting(machine, "indexed-flat").plan - const active = Configuration.normalizeConfigurationSync>( + const active = Configuration.normalizeConfigurationSync( machine, initial.state )