Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/typed-planner-transitions.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -577,9 +577,9 @@ export const normalizeConfiguration = <const States extends Machine.StateSchemas
state: Machine.Snapshot<States>
): ActiveConfiguration => configurationFromSnapshot(machine, state)

export const normalizeConfigurationSync = <const States extends Machine.StateSchemas>(
export const normalizeConfigurationSync = (
machine: Machine.Any,
state: Machine.Snapshot<States>
state: Machine.AtomicSnapshot<string, unknown>
): ActiveConfiguration => {
try {
return configurationFromSnapshot(machine, state)
Expand Down Expand Up @@ -1095,10 +1095,11 @@ const configurationFromTargetSnapshotSync = (
}
}

export const normalizeTargetConfigurationSync = <const States extends Machine.StateSchemas>(
/** 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<States> | Machine.Target<States, Machine.StateIdentifier<States>>
target: unknown
): ActiveConfiguration => {
if (isTarget(target)) {
const snapshot = target[TargetSnapshotTypeId]
Expand Down
38 changes: 12 additions & 26 deletions packages/effect-machine/src/internal/machine/invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<any, any, any>): Logic<void, never, any, any, any> => ({
const oneShot = (
effect: Effect.Effect<unknown, unknown, unknown>
): Runtime.ProcessLogic<void, unknown, unknown, unknown, unknown> => ({
initial: () => Effect.void,
run: () => effect
})
Expand All @@ -46,7 +48,7 @@ const streamLogic = (
stream: Stream.Stream<any, any, any>,
path: string,
id: string
): Runtime.ProcessLogic<void, never, any, any, void> => ({
): Runtime.ProcessLogic<void, unknown, unknown, unknown, void> => ({
initial: () => Effect.void,
run: ({ parent }) => {
const send = parent?.[Runtime.acknowledgedSend]
Expand All @@ -68,9 +70,10 @@ const resolveValue = (value: unknown, context: Omit<Machine.InvokeContext<any, a
typeof value === "function" ? value(context) : value

const makeChildOwner = (scope: Runtime.ProcessScope<any>): ChildOwner<any> => ({
spawn:
((descriptor: ChildMachine.Any, options?: { readonly input?: unknown }) =>
(scope.spawn as any)(descriptor, options)) as ChildOwner<any>["spawn"],
spawn: <const Child extends ChildMachine.Any>(
child: Child & ChildMachine.Executable<Child> & ChildMachine.ParentCompatibility<Child, any>,
...options: ChildMachine.SpawnArgs<Child>
) => scope.spawn<Child>(child, ...options),
sendTo: ((descriptor: ChildMachine.Any, event: unknown) => scope.sendTo(descriptor, event)) as ChildOwner<
any
>["sendTo"],
Expand All @@ -86,15 +89,7 @@ const resolveOne = (
return {
id: String(raw.id),
sourceName: raw.sourceName,
src: () =>
oneShot(raw.effect(context) as Effect.Effect<any, any, any>) 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,
Expand All @@ -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"
}
Expand All @@ -123,8 +110,7 @@ const resolveOne = (
return {
id,
sourceName: raw.sourceName,
src: () =>
streamLogic(raw.stream(context), path, id) as unknown as Runtime.ProcessLogic<any, any, any, any, any, any>,
src: () => streamLogic(raw.stream(context), path, id),
onDone: raw.onDone,
onFailure: raw.onFailure,
activityKind: "Stream"
Expand Down
93 changes: 20 additions & 73 deletions packages/effect-machine/src/internal/machine/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"
Expand Down Expand Up @@ -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<Requirements> = Exclude<Requirements, Scope.Scope>
type SpawnIdError<Options extends SpawnOptions> = "id" extends keyof Options ? Options extends {
readonly id?: infer Id
} ? [Id] extends [undefined] ? never : ChildAlreadyExistsError
: ChildAlreadyExistsError
: never
type SpawnError<Options extends SpawnOptions> = SpawnIdError<Options>
type SpawnResult<State, Event, Error, Requirements, Output, SpawnError, InitialError = never> = Effect.Effect<
MachineRef<State, Event, Error, Output>,
SpawnError | InitialError,
MachineRuntimeRequirement | SpawnRequirements<Requirements>
>
const Proto = {
...Inspectable.BaseProto,
...PipeablePrototype,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -965,6 +952,8 @@ export const planInitial: (
...args: ReadonlyArray<any>
) => Effect.Effect<any, PlanningError | StartupError> = 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 = <M extends Machine.Any>(
machine: M
): ReadonlyArray<
Expand Down Expand Up @@ -1122,73 +1111,31 @@ export const childFamily = <M extends Machine.Any>(machine: M): ChildMachine.Fam

export const childAddress = <Event = never>(id: string): ChildAddress<Event> => id as ChildAddress<Event>

export const spawn: {
<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, ChildInitialError = never>(
logic: Logic<
ChildState,
ChildEvent,
ChildError,
ChildRequirements,
ChildOutput,
ChildInitialError
>
): SpawnResult<ChildState, ChildEvent, ChildError, ChildRequirements, ChildOutput, never, ChildInitialError>
<
ChildState,
ChildEvent,
ChildError,
ChildRequirements,
ChildOutput,
Options extends SpawnOptions,
ChildInitialError = never
>(
logic: Logic<
ChildState,
ChildEvent,
ChildError,
ChildRequirements,
ChildOutput,
ChildInitialError
>,
options: Options & ChildAddress.OptionsCompatibility<Options, ChildEvent>
): SpawnResult<
ChildState,
ChildEvent,
ChildError,
ChildRequirements,
ChildOutput,
SpawnError<Options>,
ChildInitialError
>
} = ((logic: Logic<any, any, any, any, any, any>, options?: SpawnOptions) =>
// The public `Machine` module declares the typed overloads and checks these
// erased implementations against them structurally.
export const spawn = (logic: Logic<any, any, any, any, any, any>, options?: SpawnOptions) =>
Effect.flatMap(
internalRuntime.MachineRuntime,
(runtime) => options === undefined ? runtime.spawn(logic) : (runtime.spawn as any)(logic, options)
)) as any

export const sendTo: {
<Child extends ChildMachine.Any>(
child: Child,
event: ChildMachine.Event<Child>
): Effect.Effect<void, StoppedError, MachineRuntimeRequirement>
<Address extends ChildAddress<never>>(
id: Address,
event: ChildAddress.Event<Address>
): Effect.Effect<void, StoppedError, MachineRuntimeRequirement>
} = ((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<void, StoppedError, MachineRuntimeRequirement> =>
Effect.flatMap(
internalRuntime.MachineRuntime,
(runtime) => runtime.sendTo(child, event)
)) as any
)

export const stopChild: {
<Event>(child: ChildAddress<Event>): Effect.Effect<void, never, MachineRuntimeRequirement>
<Child extends ChildMachine.Any>(child: Child): Effect.Effect<void, never, MachineRuntimeRequirement>
} = ((child: string | ChildMachine.Any) =>
export const stopChild = (
child: string | ChildMachine.Any
): Effect.Effect<void, never, MachineRuntimeRequirement> =>
Effect.flatMap(
internalRuntime.MachineRuntime,
(runtime) => runtime.stopChild(child)
)) as any
)

export const watch = <State, Event, Error = never, Output = never>(
ref: MachineRef<State, Event, Error, Output>
Expand Down
Loading
Loading