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
7 changes: 7 additions & 0 deletions .changeset/typed-operation-boundary.md
Original file line number Diff line number Diff line change
@@ -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.
41 changes: 19 additions & 22 deletions packages/effect-machine/src/Machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2613,7 +2613,6 @@ export declare namespace Machine {
/** @internal */
readonly stateNodes: StateNodes
/** @internal */
/** @internal */
readonly handlers: any
/** @internal */
readonly initial: any
Expand Down Expand Up @@ -7737,7 +7736,7 @@ export const isFinal: <
ParentEvents
>,
state: Machine.Snapshot<States>
) => state is Machine.SnapshotContainingFinal<States, FinalStates> = internal.isFinal as any
) => state is Machine.SnapshotContainingFinal<States, FinalStates> = internal.isFinal

/**
* Defines root or nested state schemas while preserving the exact child topology.
Expand Down Expand Up @@ -7835,10 +7834,8 @@ export interface Targets<Root extends Machine.StateNodeConfig> {
* @category constructors
* @since 0.34.0
*/
export const targets: <const Root extends Machine.StateNodeConfig>(root: State<Root>) => Targets<Root> = internal
.targets as unknown as <const Root extends Machine.StateNodeConfig>(
root: State<Root>
) => Targets<Root>
export const targets: <const Root extends Machine.StateNodeConfig>(root: State<Root>) => Targets<Root> =
internal.targets

type UniqueSourceNames<F, S, T, L, C> = [
| Extract<keyof F, keyof S | keyof T | keyof L | keyof C>
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -8131,7 +8128,7 @@ type ValidateEventFields<Cases> = {
*/
export const events: <const Cases extends Readonly<Record<string, Schema.Struct.Fields>>>(
cases: Cases & ValidateEventFields<NoInfer<Cases>> & ValidateEventProtocolBuilder<"public", EventFieldsSchemas<Cases>>
) => Machine.EventProtocol<"public", EventFieldsSchemas<Cases>> = internal.eventsFromFields as any
) => Machine.EventProtocol<"public", EventFieldsSchemas<Cases>> = internal.eventsFromFields

/** Imports existing schemas and protocols without rebuilding their contracts.
* @category constructors
Expand All @@ -8141,7 +8138,7 @@ export const eventsFromSchemas: {
<const Inputs extends ReadonlyArray<Machine.EventProtocolInput<"public">>>(
...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
Expand Down Expand Up @@ -8201,7 +8198,7 @@ export const internalEvents: <const Cases extends Readonly<Record<string, Schema
& Cases
& ValidateEventFields<NoInfer<Cases>>
& ValidateEventProtocolBuilder<"internal", EventFieldsSchemas<Cases>>
) => Machine.EventProtocol<"internal", EventFieldsSchemas<Cases>> = internal.internalEventsFromFields as any
) => Machine.EventProtocol<"internal", EventFieldsSchemas<Cases>> = internal.internalEventsFromFields

/** Imports existing schemas and protocols without rebuilding their contracts.
* @category constructors
Expand All @@ -8211,7 +8208,7 @@ export const internalEventsFromSchemas: {
<const Inputs extends ReadonlyArray<Machine.EventProtocolInput<"internal">>>(
...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
Expand All @@ -8238,7 +8235,7 @@ export const emittedEvents: <const Cases extends Readonly<Record<string, Schema.
& Cases
& ValidateEventFields<NoInfer<Cases>>
& ValidateEventProtocolBuilder<"emitted", EventFieldsSchemas<Cases>>
) => Machine.EventProtocol<"emitted", EventFieldsSchemas<Cases>> = internal.emittedEventsFromFields as any
) => Machine.EventProtocol<"emitted", EventFieldsSchemas<Cases>> = internal.emittedEventsFromFields

/** Imports existing schemas and protocols without rebuilding their contracts.
* @category constructors
Expand All @@ -8248,7 +8245,7 @@ export const emittedEventsFromSchemas: {
<const Inputs extends ReadonlyArray<Machine.EventProtocolInput<"emitted">>>(
...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.
Expand Down Expand Up @@ -8342,7 +8339,7 @@ export const encodeSnapshot: <
Machine.EncodedSnapshot,
MachineSchemaEncodeError,
Machine.SnapshotEncodingServices<States>
> = internal.encodeSnapshot as any
> = internal.encodeSnapshot

/**
* Decodes a normalized data representation into a validated machine snapshot.
Expand Down Expand Up @@ -8430,7 +8427,7 @@ export const decodeSnapshot: <
Machine.Snapshot<States>,
MachineSchemaDecodeError,
Machine.SnapshotDecodingServices<States>
> = internal.decodeSnapshot as any
> = internal.decodeSnapshot

type TransitionBranchRecordError<Message extends string, Key extends PropertyKey = never> = {
readonly "~effect/Machine/TransitionBranchRecordError": Message
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -8738,7 +8735,7 @@ export const enabled: <
ParentEvents
>,
state: Machine.Snapshot<States>
) => ReadonlyArray<Machine.TagOf<Events[number]>> = internal.enabled as any
) => ReadonlyArray<Machine.TagOf<Events[number]>> = internal.enabled

/**
* Tests whether a concrete event would select at least one transition from a
Expand Down Expand Up @@ -8870,7 +8867,7 @@ export const can: {
state: Machine.Snapshot<States>,
event: Machine.EventInputOf<Events>
): Effect.Effect<boolean, MachineSchemaDecodeError>
} = internal.can as any
} = internal.can

/**
* Returns an Effect that plans the next state snapshot without running command effects.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -9315,7 +9312,7 @@ export const prepare: <
Machine.EmittedEventOf<Emits>
>
>
> = internal.prepare as any
> = internal.prepare

/**
* Starts a machine.
Expand Down Expand Up @@ -9429,7 +9426,7 @@ export const start: <
Machine.EventOf<Events>,
Machine.EmittedEventOf<Emits>
>
> = internal.start as any
> = internal.start

/**
* Starts a fresh managed runtime from a decoded logical snapshot.
Expand Down Expand Up @@ -9541,4 +9538,4 @@ export const resume: <
Machine.EventOf<Events>,
Machine.EmittedEventOf<Emits>
>
> = internal.resume as any
> = internal.resume
11 changes: 4 additions & 7 deletions packages/effect-machine/src/internal/machine/atom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const resumeMachineAtomEffect = (
get: Atom.AtomContext,
machine: Machine.Machine.Any,
snapshot: Machine.Machine.Snapshot<any>
) => runMachineAtomEffect(get, internalMachine.resume(machine as any, snapshot as any))
) => runMachineAtomEffect(get, internalMachine.resume(machine, snapshot))

type RefState<Ref> = Ref extends Machine.MachineRef<infer State, any, any, any, any> ? State : never
type RefError<Ref> = Ref extends Machine.MachineRef<any, any, infer Error, any, any> ? Error : never
Expand Down Expand Up @@ -723,10 +723,7 @@ export const can = (event: unknown) => {
event: unknown
) => Effect.Effect<boolean, Machine.MachineSchemaDecodeError> = machine === undefined
? () => Effect.die(new Error("AtomMachine.can requires a machine atom created by AtomMachine"))
: internalMachine.can(machine) as (
state: Machine.Machine.Snapshot<any>,
event: unknown
) => Effect.Effect<boolean, Machine.MachineSchemaDecodeError>
: internalMachine.can(machine)

const result = Atom.readable((get): AsyncResult.AsyncResult<boolean, any> => {
const current = get(self.snapshot)
Expand Down Expand Up @@ -840,7 +837,7 @@ export const make: {
Machine.Machine.EmittedEventOf<Emits>
>
} = ((machine: Machine.Machine.Any, ...args: ReadonlyArray<unknown>) => {
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)
Expand Down Expand Up @@ -869,7 +866,7 @@ const makeWithRuntime = (
machine: Machine.Machine.Any,
args: ReadonlyArray<unknown>
): MachineAtom<any, any, any, any, any, any> => {
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)
Expand Down
6 changes: 3 additions & 3 deletions packages/effect-machine/src/internal/machine/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -13,7 +13,7 @@ interface Source {
}
export interface Declaration {
readonly initialize?: (input: unknown) => unknown
readonly root: State<Machine.StateNodeConfig>
readonly root: Machine.Any["root"]
readonly sources: ReadonlyMap<string, Source>
readonly branches: ReadonlyMap<string, Readonly<Record<string, Readonly<Record<string, unknown>>>>>
}
Expand All @@ -22,7 +22,7 @@ const record = (value: unknown, message: string): Record<string, unknown> => {
return value as Record<string, unknown>
}
export const capture = (
root: State<Machine.StateNodeConfig>,
root: Machine.Any["root"],
config: Readonly<Record<string, unknown>>
): Declaration => {
const sources = new Map<string, Source>()
Expand Down
23 changes: 22 additions & 1 deletion packages/effect-machine/src/internal/machine/errors.ts
Original file line number Diff line number Diff line change
@@ -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"

/**
Expand Down Expand Up @@ -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<never, PlanningError> =>
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) })
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -52,7 +52,7 @@ interface Captured {
readonly handlers: Record<string, unknown>
}
export const capture = (
root: State<Machine.StateNodeConfig>,
root: Machine.Any["root"],
handler: Readonly<Record<string, unknown>>
): Captured => {
const visit = (
Expand Down
Loading
Loading