diff --git a/.changeset/bright-machines-reference.md b/.changeset/bright-machines-reference.md new file mode 100644 index 0000000..c8b89f8 --- /dev/null +++ b/.changeset/bright-machines-reference.md @@ -0,0 +1,15 @@ +--- +"@typeonce/effect-machine": minor +--- + +Rename the minimal inter-machine reference types so they use machine terminology and remain distinct from Effect Cluster concepts. + +```ts +Machine.ActorRef // before +Machine.MachineTarget // after + +Machine.ActorContext // before +Machine.MachineReferences // after +``` + +The inferred `self` and `parent` fields and all runtime behavior are unchanged. diff --git a/README.md b/README.md index dde42af..c5c4fdf 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ data, put it on their compound parent. ### Separate inputs, raised events, and emissions -`events` is the public actor-input protocol. Events raised to the same machine +`events` is the public machine-input protocol. Events raised to the same machine belong in `internalEvents`. Ephemeral outward notifications have their own `emittedEvents` protocol: @@ -186,7 +186,7 @@ protocols but cannot expose a finite constructor set; pass a complete event object to `send` or `Machine.plan` for those events. `ref.emissions` is a hot `Stream`: it publishes only notifications produced -after subscription, replays nothing, and completes when the actor terminates. +after subscription, replays nothing, and completes when the machine terminates. Snapshots remain separate and stateful: `ref.changes` begins with the current lifecycle snapshot and then follows later changes. Use `Machine.prepare` when an observer must be installed before initial-entry actions run: @@ -209,10 +209,10 @@ emission: the observer is simply subscribed before initialization begins. Invalid event and emission constructions fail the machine with a typed `MachineSchemaDecodeError`; they do not throw from the constructor call. -### Send explicitly between actors +### Send explicitly between machines -`raise` targets the current machine in the same macrostep. `sendTo` targets an -actor mailbox and is processed later. A child declares the subset of parent +`raise` targets the current machine in the same macrostep. `sendTo` targets a +machine mailbox and is processed later. A child declares the subset of parent inputs it may send with `parentEvents`: ```ts @@ -244,15 +244,18 @@ const ParentInputs = Machine.events(Start, ParentEvents) The same child remains isolated and may be started as a root, where `parent` is `undefined`. When `Child` is invoked, the parent definition must accept every event in `parentEvents`; otherwise `.handle(...)` is a compile-time error. -Inside the child, the parent reference accepts only those declared events. -`emit` never sends to the parent: it only publishes on the emitting actor's +Inside the child, the parent target accepts only those declared events. +`emit` never sends to the parent: it only publishes on the emitting machine's `emissions` stream. Every handler also receives `self`, which can be targeted with `sendTo` when a later mailbox turn is required. Use `raise` instead for same-macrostep work. +Both `self` and `parent` are minimal `Machine.MachineTarget` values. The +shared `Machine.MachineReferences` context keeps +their input protocols separate without exposing snapshot or lifecycle APIs. Structural state values use distinct names: `containingState` is the immediate valued state in the same statechart, while `ancestors` maps valued ancestor -paths. `parent` always means the owning actor reference. +paths. `parent` always means the owning machine target. ### Choose the target by scope @@ -401,7 +404,7 @@ const childEmissions = AtomMachine.childEmissions(counterAtom.child(Worker)) ``` These streams require the same `AtomRegistry`, follow the currently mounted -actor instance, and do not replay notifications from an earlier subscription +machine instance, and do not replay notifications from an earlier subscription or child instance. ## Persistence diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 96ea0bd..f27e546 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -88,7 +88,7 @@ the deferred constructors preserve that identity after decoding. - Reuse an exported child descriptor for inline invocation, `sendTo`, and child lookup. Independently constructed descriptors are equivalent only when both their id and machine identity match. -- `events` is the public actor-input protocol. `internalEvents` contains +- `events` is the public machine-input protocol. `internalEvents` contains machine-local raised events. `parentEvents` describes the public events a child may send to its owner. `emittedEvents` describes outward ephemeral notifications and is never delivered implicitly to a parent. @@ -114,7 +114,7 @@ its extra control is required: invocation is addressable only when `Machine.invoke` receives that address explicitly. - Use the callback's `enqueue` argument for `raise`, `emit`, `sendTo`, and - `stop`. These operations record closed actor commands and do not run Effects. + `stop`. These operations record closed machine commands and do not run Effects. ## Atomic, compound, parallel, and history states @@ -321,7 +321,7 @@ the default. The machine's readiness type tracks missing defaults and shallow initializers. History is an overwriteable register, not a stack: restoration does not consume it, and the next parent exit replaces it. Entry actions and invokes run again; -prior effects, actors, and timers are not rewound. +prior effects, machine instances, and timers are not rewound. ## Choosing a target @@ -407,7 +407,7 @@ only schema-backed paths; use `matches` or `getSnapshot` for any active path. `context.containingState` is the immediate typed state value (`undefined` at a root or when that state is schema-less). `context.ancestors` contains only valued structural ancestors. This is separate from `context.parent`, which is -the owning actor reference or `undefined` for a root actor. Use full state paths +the owning machine target or `undefined` for a root machine. Use full state paths when another ancestor value is needed: ```ts @@ -491,7 +491,7 @@ are an upper bound on concrete destinations, not an exhaustive result set. `reenter: true` remains meaningful with `target.none()`: the source exits and enters again while its logical configuration is retained. -Closed statechart and actor operations use `enqueue`: +Closed statechart and machine operations use `enqueue`: ```ts Submit: ({ target }, enqueue) => { @@ -500,7 +500,7 @@ Submit: ({ target }, enqueue) => { } ``` -Declare emission constructors separately from actor inputs: +Declare emission constructors separately from machine inputs: ```ts const Emissions = Machine.emittedEvents(SaveRequested, AuditRecorded) @@ -514,9 +514,9 @@ const definition = Machine.make({ ``` `enqueue.raise(...)` is a same-macrostep input to self. `enqueue.sendTo(...)` -targets an actor mailbox and is processed later. `enqueue.emit(...)` is neither: +targets a machine mailbox and is processed later. `enqueue.emit(...)` is neither: it publishes a one-off outward notification. Observe it with -`ref.emissions`, a hot non-replayed `Stream` that completes with the actor. +`ref.emissions`, a hot non-replayed `Stream` that completes with the machine. `ref.changes` is stateful and begins with the current lifecycle snapshot. Use `Machine.prepare(machine)` to obtain `changes` and `emissions` before initialization. Subscribe to the desired stream and then evaluate @@ -564,10 +564,12 @@ const parent = Machine.make({ Invoking the child under a parent that lacks any required `parentEvents` case is a type error. Within child handlers, `parent` accepts only that protocol. The same child may run as a root, where `parent` is `undefined`. `self` accepts -the machine's public inputs. Neither actor reference is a structural state -value; use `containingState` and `ancestors` for statechart ancestry. +the machine's public inputs. Both are minimal `MachineTarget` values, +provided by the shared `MachineReferences` handler +context. Neither machine target is a structural state value; use +`containingState` and `ancestors` for statechart ancestry. -Atom-backed actors retain the same transient semantics. Use +Atom-backed machines retain the same transient semantics. Use `AtomMachine.emissions(machineAtom)` for a root and `AtomMachine.childEmissions(childAtom)` for the currently active child. Both return streams requiring the corresponding `AtomRegistry`; emissions are not diff --git a/examples/platformer/src/main.ts b/examples/platformer/src/main.ts index 0eeb008..4598149 100644 --- a/examples/platformer/src/main.ts +++ b/examples/platformer/src/main.ts @@ -65,11 +65,11 @@ const publish = (next: CharacterSnapshot) => { } const program = Effect.gen(function*() { - const actor = yield* Machine.start(CharacterMachine) - deliver = (event) => Effect.runFork(actor.send(event).pipe(Effect.catchTag("StoppedError", () => Effect.void))) - publish(yield* actor.state) - for (const event of pending.splice(0)) yield* actor.send(event) - yield* Stream.runForEach(actor.changes, ({ state }) => Effect.sync(() => publish(state))) + const ref = yield* Machine.start(CharacterMachine) + deliver = (event) => Effect.runFork(ref.send(event).pipe(Effect.catchTag("StoppedError", () => Effect.void))) + publish(yield* ref.state) + for (const event of pending.splice(0)) yield* ref.send(event) + yield* Stream.runForEach(ref.changes, ({ state }) => Effect.sync(() => publish(state))) }) const fiber = Effect.runFork(program) diff --git a/examples/playground/src/examples/worker-tabs/machine.worker.ts b/examples/playground/src/examples/worker-tabs/machine.worker.ts index 6e2c9b9..80dc748 100644 --- a/examples/playground/src/examples/worker-tabs/machine.worker.ts +++ b/examples/playground/src/examples/worker-tabs/machine.worker.ts @@ -14,8 +14,8 @@ let deliver: ((event: SharedEvent) => void) | undefined const pendingEvents: Array = [] const program = Effect.gen(function*() { - const actor = yield* Machine.start(SharedMachine) - const initial = yield* actor.snapshot + const ref = yield* Machine.start(SharedMachine) + const initial = yield* ref.snapshot if (initial.status === "error") { post({ _tag: "WorkerError", message: "The worker-hosted machine failed during startup." }) @@ -30,7 +30,7 @@ const program = Effect.gen(function*() { deliver = (event) => { Effect.runFork( - actor.send(event).pipe( + ref.send(event).pipe( Effect.catchTag( "StoppedError", () => Effect.sync(() => post({ _tag: "WorkerError", message: "The worker-hosted machine has stopped." })) @@ -43,7 +43,7 @@ const program = Effect.gen(function*() { post({ _tag: "Ready" }) - yield* Stream.runForEach(actor.changes, (snapshot) => + yield* Stream.runForEach(ref.changes, (snapshot) => Effect.sync(() => { if (snapshot.status === "error") { post({ _tag: "WorkerError", message: "The worker-hosted machine failed while processing an event." }) diff --git a/src/Machine.ts b/src/Machine.ts index ca2e0c7..48a7cc5 100644 --- a/src/Machine.ts +++ b/src/Machine.ts @@ -175,18 +175,18 @@ export interface Machine< > /** - * Ephemeral outward notifications published to this actor's observers. - * Emissions are never delivered implicitly to an owning actor. + * Ephemeral outward notifications published to this machine's observers. + * Emissions are never delivered implicitly to an owning machine. * * @since 0.4.0 */ readonly emittedEvents: Machine.EventProtocol<"emitted", Emits> /** - * Public input events accepted by an owning actor when this machine is + * Public input events accepted by an owning machine when this machine is * running as a child. * - * The protocol types the optional `parent` actor reference exposed to + * The protocol types the optional `parent` machine target exposed to * handlers and is checked against a concrete parent's public events at * composition boundaries. * @@ -387,7 +387,7 @@ export interface Runtime { readonly raise: (event: Machine.EventInput) => Effect.Effect /** - * Publishes an ephemeral notification to the running actor's observers. + * Publishes an ephemeral notification to the running machine's observers. * * @since 0.4.0 */ @@ -397,37 +397,37 @@ export interface Runtime { } /** - * Minimal typed reference accepted by targeted actor commands. + * Minimal typed target accepted by inter-machine send commands. * * @category models - * @since 0.10.0 + * @since 0.12.0 */ -export interface ActorRef { +export interface MachineTarget { readonly id: string readonly sessionId: string readonly send: (event: Event) => Effect.Effect } /** - * Actor references available while evaluating machine behavior. + * Machine targets available while evaluating machine behavior. * * @category models - * @since 0.10.0 + * @since 0.12.0 */ -export interface ActorContext< +export interface MachineReferences< InputEvents extends ReadonlyArray, ParentEvents extends ReadonlyArray > { - /** Reference to the current actor. Sending queues a later mailbox event. */ - readonly self: ActorRef> + /** Target for the current machine. Sending queues a later mailbox event. */ + readonly self: MachineTarget> - /** Reference to the owning actor, or `undefined` when running as a root. */ - readonly parent: ActorRef> | undefined + /** Target for the owning machine, or `undefined` when running as a root. */ + readonly parent: MachineTarget> | undefined } /** * Synchronous commands available while a machine transition is being - * selected. Enqueuing only records statechart and actor operations; it never + * selected. Enqueuing only records statechart and machine operations; it never * executes an Effect. * * @category models @@ -437,12 +437,12 @@ export interface Enqueue { /** Raises an event inside the current macrostep. */ readonly raise: (event: Machine.EventInput) => void - /** Publishes an ephemeral notification to this actor's observers. */ + /** Publishes an ephemeral notification to this machine's observers. */ readonly emit: (event: Machine.EmittedEventInput) => void /** Sends an event to an invoked child after the transition is selected. */ readonly sendTo: { - (target: ActorRef, event: Event): void + (target: MachineTarget, event: Event): void (child: Child, event: ChildMachine.Event): void
>(child: Address, event: ChildAddress.Event
): void } @@ -455,7 +455,7 @@ export interface Enqueue { } /** - * A closed actor command recorded by a synchronous machine transition. + * A closed machine command recorded by a synchronous transition. * * @category models * @since 0.4.0 @@ -463,7 +463,7 @@ export interface Enqueue { export type Command = | { readonly _tag: "SendTo" - readonly target: ActorRef | ChildMachine.Any | ChildAddress + readonly target: MachineTarget | ChildMachine.Any | ChildAddress readonly event: unknown } | { @@ -1745,7 +1745,7 @@ export interface Prepared - extends ActorRef + extends MachineTarget { /** Stable machine definition id, or a generated fallback when none was declared. */ readonly id: string @@ -1764,7 +1764,7 @@ export interface MachineRef @@ -1892,9 +1892,9 @@ export declare namespace Logic { /** Starts a child process owned by this scope. */ readonly spawn: Spawn - /** Sends an event to an actor reference or typed parent-local child address. */ + /** Sends an event to a machine target or typed parent-local child address. */ readonly sendTo: { - (target: ActorRef, event: TargetEvent): Effect.Effect + (target: MachineTarget, event: TargetEvent): Effect.Effect
>( id: Address, event: ChildAddress.Event
@@ -2276,7 +2276,7 @@ export declare namespace Machine { */ export type InputEvents = M[typeof MachineTypeId]["inputEvents"] - /** Extracts the public input protocol required from an owning actor. */ + /** Extracts the public input protocol required from an owning machine. */ export type ParentEvents = M[typeof MachineTypeId]["parentEvents"] /** Extracts an internal event schema tuple from the complete and public protocols. */ @@ -4011,7 +4011,7 @@ export declare namespace Machine { R, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly state: StateByIdentifier readonly containingState: ParentStateValue readonly ancestors: ParentStateValues @@ -4041,7 +4041,7 @@ export declare namespace Machine { StateId extends StateIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly state: StateByIdentifier readonly containingState: ParentStateValue readonly ancestors: ParentStateValues @@ -4061,7 +4061,7 @@ export declare namespace Machine { StateId extends StateIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly state: StateByIdentifier readonly containingState: ParentStateValue readonly ancestors: ParentStateValues @@ -4084,7 +4084,7 @@ export declare namespace Machine { Output, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly id: string readonly state: StateByIdentifier readonly containingState: ParentStateValue @@ -4107,7 +4107,7 @@ export declare namespace Machine { Output, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly id: string readonly state: StateByIdentifier readonly containingState: ParentStateValue @@ -4126,7 +4126,7 @@ export declare namespace Machine { Error, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly id: string readonly state: StateByIdentifier readonly containingState: ParentStateValue @@ -4149,7 +4149,7 @@ export declare namespace Machine { StateId extends StateIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly state: StateByIdentifier readonly containingState: ParentStateValue readonly ancestors: ParentStateValues @@ -4180,7 +4180,7 @@ export declare namespace Machine { StateId extends StateIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly state: StateByIdentifier readonly containingState: ParentStateValue readonly ancestors: ParentStateValues @@ -4207,7 +4207,7 @@ export declare namespace Machine { ChoiceId extends ChoiceIdentifier, InputEvents extends ReadonlyArray = Events, ParentEvents extends ReadonlyArray = readonly [] - > extends ActorContext { + > extends MachineReferences { readonly containingState: StateByIdentifier< States, Extract, StateIdentifier> @@ -6431,7 +6431,7 @@ interface Make { * adds raised events and other machine-local deliveries. * `Machine.emittedEvents` defines outward ephemeral notifications, while * `parentEvents` declares the inputs a child may explicitly send to its owning - * actor. All descriptors expose deferred constructors while retaining their + * machine. All descriptors expose deferred constructors while retaining their * schemas opaquely for runtime validation. Public and internal tags must be * disjoint. * @@ -6543,8 +6543,8 @@ export const internalEvents: { /** * Defines the ephemeral notifications a machine may publish to external - * observers. Emitted events are separate from actor input and are never sent - * implicitly to a parent actor. Observe them through `MachineRef.emissions` or + * observers. Emitted events are separate from machine input and are never sent + * implicitly to a parent machine. Observe them through `MachineRef.emissions` or * the AtomMachine emission stream adapters. * * @category constructors @@ -7379,11 +7379,11 @@ export const invoke: { > } = ((config: unknown) => config) as any /** - * Plans the initial state for a machine without executing actor commands. + * Plans the initial state for a machine without executing machine commands. * * **Details** * - * The returned plan contains the settled initial snapshot, actor commands, + * The returned plan contains the settled initial snapshot, machine commands, * emitted events, optional final output, and every startup microstep. Planning * may evaluate transition logic and follow completion, eventless, and * raised-event steps. Transition callbacks are evaluated synchronously. @@ -7392,8 +7392,8 @@ export const invoke: { * * **Gotchas** * - * `start` executes the closed command list as part of the managed actor commit - * protocol. Manual planners may inspect commands but need a running actor scope + * `start` executes the closed command list as part of the managed machine commit + * protocol. Manual planners may inspect commands but need running machine targets * to execute child-addressed operations. * * **Example** @@ -7628,7 +7628,7 @@ export const enabled: < * * **Gotchas** * - * `plan` returns data; it does not implement the actor commit protocol. + * `plan` returns data; it does not implement the managed machine commit protocol. * `start` executes child commands, publishes `next`, and then delivers * `emittedEvents`. Events with no enabled transition are ignored and produce * an unchanged plan. @@ -8029,7 +8029,7 @@ export const prepare: < * **Details** * * For each accepted event the runtime plans the complete synchronous - * macrostep, executes closed actor commands, stops invokes for exited states, + * macrostep, executes closed machine commands, stops invokes for exited states, * publishes the new state, delivers emitted events, and then starts invokes * for entered states. * diff --git a/src/internal/machine/cluster.ts b/src/internal/machine/cluster.ts index 00fa0bd..c9cfade 100644 --- a/src/internal/machine/cluster.ts +++ b/src/internal/machine/cluster.ts @@ -317,7 +317,7 @@ export const make = < if (initial.commands.length > 0) { return yield* fail( "UnsupportedProcessLocal", - "Machine actor commands require a managed local process" + "Machine commands require a managed local process" ) } current = initial.state @@ -329,7 +329,7 @@ export const make = < if (planned.commands.length > 0) { return yield* fail( "UnsupportedProcessLocal", - "Machine actor commands require a managed local process" + "Machine commands require a managed local process" ) } current = planned.next diff --git a/src/internal/machine/commandRuntime.ts b/src/internal/machine/commandRuntime.ts index 5cbc202..da2ffe1 100644 --- a/src/internal/machine/commandRuntime.ts +++ b/src/internal/machine/commandRuntime.ts @@ -10,7 +10,9 @@ import type { RuntimeCommand } from "./command.js" import { decodeEmit, decodeEvent } from "./protocol.js" import type { ProcessScope } from "./runtime.js" -const isActorRef = (target: unknown): target is { readonly send: (event: unknown) => Effect.Effect } => +const isMachineTarget = ( + target: unknown +): target is { readonly send: (event: unknown) => Effect.Effect } => typeof target === "object" && target !== null && "send" in target && typeof target.send === "function" export const makeLiveRuntime = ( @@ -33,7 +35,7 @@ export const runCommands = ( ) => Effect.forEach(commands, (command) => command._tag === "SendTo" - ? isActorRef(command.target) + ? isMachineTarget(command.target) ? command.target.send(command.event) : scope.sendTo(command.target as never, command.event) : scope.stopChild(command.child as never), { discard: true }) diff --git a/src/internal/machine/configuration.ts b/src/internal/machine/configuration.ts index 284a27f..987488b 100644 --- a/src/internal/machine/configuration.ts +++ b/src/internal/machine/configuration.ts @@ -8,7 +8,7 @@ import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import * as Option from "effect/Option" import { hasProperty } from "effect/Predicate" -import type { ActorRef, Machine } from "../../Machine.js" +import type { Machine, MachineTarget } from "../../Machine.js" import { MachineSchemaDecodeError } from "./errors.js" import { decodeBoundary, @@ -252,25 +252,25 @@ export interface ActiveConfiguration { readonly values: ReadonlyMap readonly outputs: ReadonlyMap readonly history: ReadonlyMap - readonly actorScope?: PlanningActorScope + readonly machineReferences?: PlanningMachineReferences } -export interface PlanningActorScope { - readonly self: ActorRef - readonly parent: ActorRef | undefined +export interface PlanningMachineReferences { + readonly self: MachineTarget + readonly parent: MachineTarget | undefined } -export const withActorScope = ( +export const withMachineReferences = ( configuration: ActiveConfiguration, - actorScope: PlanningActorScope + machineReferences: PlanningMachineReferences ): ActiveConfiguration => ({ ...configuration, - actorScope: { self: actorScope.self, parent: actorScope.parent } + machineReferences: { self: machineReferences.self, parent: machineReferences.parent } }) -export const getActorScope = ( +export const getMachineReferences = ( configuration: ActiveConfiguration -): PlanningActorScope | undefined => configuration.actorScope +): PlanningMachineReferences | undefined => configuration.machineReferences export interface FinalCompletion { readonly path: string diff --git a/src/internal/machine/executionPlan.ts b/src/internal/machine/executionPlan.ts index 9a29146..38c04ff 100644 --- a/src/internal/machine/executionPlan.ts +++ b/src/internal/machine/executionPlan.ts @@ -5,7 +5,7 @@ */ import * as Effect from "effect/Effect" -import type { ActorRef, Machine } from "../../Machine.js" +import type { Machine, MachineTarget } from "../../Machine.js" import { getTargetBuilder, type RuntimeCommand } from "./command.js" import { type ActiveConfiguration, @@ -18,10 +18,10 @@ import { isDescendantOf, normalizeConfigurationSync, normalizeTargetConfigurationSync, - type PlanningActorScope, + type PlanningMachineReferences, snapshotFromConfiguration, validateInitialConfiguration, - withActorScope + withMachineReferences } from "./configuration.js" import { InfiniteTransitionError, StoppedError } from "./errors.js" import * as InvocationEvent from "./invocationEvent.js" @@ -67,28 +67,28 @@ interface IndexedExecutionDescriptor { > } -const planningActorScopes = new WeakMap() +const planningMachineReferences = new WeakMap() -const getPlanningActorScope = (machine: Machine.Any): PlanningActorScope => { - const cached = planningActorScopes.get(machine) +const getPlanningMachineReferences = (machine: Machine.Any): PlanningMachineReferences => { + const cached = planningMachineReferences.get(machine) if (cached !== undefined) return cached - const self: ActorRef = { + const self: MachineTarget = { id: machine.id ?? "Machine", sessionId: "Machine.plan", send: () => Effect.fail(new StoppedError()) } const scope = { self, parent: undefined } - planningActorScopes.set(machine, scope) + planningMachineReferences.set(machine, scope) return scope } -const getActorContext = ( +const resolveMachineReferences = ( machine: Machine.Any, - actorScope: PlanningActorScope | undefined -): PlanningActorScope => - actorScope === undefined - ? getPlanningActorScope(machine) - : { self: actorScope.self, parent: actorScope.parent } + machineReferences: PlanningMachineReferences | undefined +): PlanningMachineReferences => + machineReferences === undefined + ? getPlanningMachineReferences(machine) + : { self: machineReferences.self, parent: machineReferences.parent } const indexedStateConfigKeys: ReadonlySet = new Set([ "initial", @@ -363,7 +363,7 @@ const makeIndexedTransitionContext = ( configuration: OwnedIndexedState, sourceIndex: number, event: any, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ): any => { const source = descriptor.nodes[sourceIndex]! const parentIndex = descriptor.parentIndices[sourceIndex]! @@ -372,7 +372,7 @@ const makeIndexedTransitionContext = ( ancestors[descriptor.nodes[ancestorIndex]!.path] = configuration.values[ancestorIndex] } return { - ...getActorContext(machine, actorScope), + ...resolveMachineReferences(machine, machineReferences), state: configuration.values[sourceIndex], containingState: parentIndex < 0 ? undefined : configuration.values[parentIndex], ancestors, @@ -454,7 +454,7 @@ const selectIndexedEventTransitions = ( descriptor: IndexedExecutionDescriptor, configuration: OwnedIndexedState, event: any, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ): ReadonlyArray => { const selected: Array = [] for (const leafIndex of configuration.activeLeaves) { @@ -476,7 +476,7 @@ const selectIndexedEventTransitions = ( configuration, sourceIndex, event, - actorScope + machineReferences ) }) } @@ -660,7 +660,7 @@ const planIndexedFlatState = ( configuration: OwnedIndexedState, decoded: { readonly _tag: PropertyKey }, retainMicrosteps: boolean, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ): ExecutionMacrostep => { let current = configuration let event: any = decoded @@ -712,7 +712,7 @@ const planIndexedFlatState = ( machine, transition.transition, { - ...getActorContext(machine, actorScope), + ...resolveMachineReferences(machine, machineReferences), state: current.values[sourceIndex], containingState: undefined, ancestors: {}, @@ -807,7 +807,7 @@ const planIndexedState = ( configuration: OwnedIndexedState, input: unknown, retainMicrosteps: boolean, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ): ExecutionMacrostep => { if (InvocationEvent.isInvocationEvent(input)) { // Invocation lifecycle transitions retain the generic planner as their @@ -816,9 +816,9 @@ const planIndexedState = ( // the representation boundary. const planned = planConfiguration( machine as any, - actorScope === undefined + machineReferences === undefined ? activeConfigurationFromIndexedState(descriptor, configuration) - : withActorScope(activeConfigurationFromIndexedState(descriptor, configuration), actorScope), + : withMachineReferences(activeConfigurationFromIndexedState(descriptor, configuration), machineReferences), input ) return { @@ -841,7 +841,7 @@ const planIndexedState = ( } const decoded = decodeEventSync(machine, input) if (descriptor.flat) { - return planIndexedFlatState(machine, descriptor, configuration, decoded, retainMicrosteps, actorScope) + return planIndexedFlatState(machine, descriptor, configuration, decoded, retainMicrosteps, machineReferences) } if (descriptor.finalIndices.some((index) => configuration.active[index] === 1)) { const active = activeConfigurationFromIndexedState(descriptor, configuration) @@ -862,7 +862,7 @@ const planIndexedState = ( } } - const selections = selectIndexedEventTransitions(machine, descriptor, configuration, decoded, actorScope) + const selections = selectIndexedEventTransitions(machine, descriptor, configuration, decoded, machineReferences) if (selections.length === 0) { return { next: configuration, @@ -930,7 +930,7 @@ const planIndexedState = ( } raisedIndex += 1 currentEvent = raised - const raisedSelections = selectIndexedEventTransitions(machine, descriptor, current, raised, actorScope) + const raisedSelections = selectIndexedEventTransitions(machine, descriptor, current, raised, machineReferences) if (raisedSelections.length === 0) continue const step = indexedMicrostep(machine, descriptor, current, raised, raisedSelections) current = step.next @@ -949,11 +949,11 @@ export interface CompiledExecutionPlan { state: unknown, event: unknown, retainMicrosteps?: boolean, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ) => ExecutionMacrostep readonly initial?: ( args: ReadonlyArray, - actorScope?: PlanningActorScope + machineReferences?: PlanningMachineReferences ) => { readonly state: Machine.Snapshot readonly configuration: unknown @@ -970,12 +970,12 @@ const makeActiveExecutionPlan = (machine: Machine.Any): CompiledExecutionPlan => fromConfiguration: (configuration) => configuration, toConfiguration: (state) => state as ActiveConfiguration, snapshot: (state) => snapshotFromConfiguration(machine, state as ActiveConfiguration), - plan: (state, event, _retainMicrosteps, actorScope) => + plan: (state, event, _retainMicrosteps, machineReferences) => planConfiguration( machine as any, - actorScope === undefined + machineReferences === undefined ? state as ActiveConfiguration - : withActorScope(state as ActiveConfiguration, actorScope), + : withMachineReferences(state as ActiveConfiguration, machineReferences), event as any ) }) @@ -987,9 +987,9 @@ const makeIndexedExecutionPlan = ( fromConfiguration: (configuration) => ownedIndexedStateFromActive(indexed, configuration), toConfiguration: (state) => activeConfigurationFromIndexedState(indexed, state as OwnedIndexedState), snapshot: (state) => snapshotFromIndexedState(indexed, state as OwnedIndexedState), - plan: (state, event, retainMicrosteps = false, actorScope) => - planIndexedState(machine, indexed, state as OwnedIndexedState, event, retainMicrosteps, actorScope), - initial: (args, actorScope) => { + plan: (state, event, retainMicrosteps = false, machineReferences) => + planIndexedState(machine, indexed, state as OwnedIndexedState, event, retainMicrosteps, machineReferences), + initial: (args, machineReferences) => { const inputArgs = machine.input === undefined ? args : args.length === 0 @@ -997,7 +997,7 @@ const makeIndexedExecutionPlan = ( : [decodeInputSync(machine, machine.input, args[0])] const initial = machine.initial(...inputArgs as any) const normalized = normalizeConfigurationSync(machine, initial as Machine.Snapshot) - const active = actorScope === undefined ? normalized : withActorScope(normalized, actorScope) + const active = machineReferences === undefined ? normalized : withMachineReferences(normalized, machineReferences) validateInitialConfiguration(machine, active) const completed = completeConfigurationSync(machine, active, InitialEvent).configuration const configuration = ownedIndexedStateFromActive(indexed, completed) diff --git a/src/internal/machine/invocation.ts b/src/internal/machine/invocation.ts index 8cc8bd3..9769aa4 100644 --- a/src/internal/machine/invocation.ts +++ b/src/internal/machine/invocation.ts @@ -235,7 +235,7 @@ export const startAll = ( .filter((path) => configuration.active.has(path)) .flatMap((path) => { const context = { - ...(Configuration.getActorScope(configuration) ?? { self: scope.self, parent: scope.parent }), + ...(Configuration.getMachineReferences(configuration) ?? { self: scope.self, parent: scope.parent }), state: configuration.values.get(path), containingState: Configuration.getParentValue(machine, configuration, path), ancestors: Configuration.getParentValues(machine, configuration, path), diff --git a/src/internal/machine/planner.ts b/src/internal/machine/planner.ts index b1bbf27..eb6c184 100644 --- a/src/internal/machine/planner.ts +++ b/src/internal/machine/planner.ts @@ -7,7 +7,13 @@ import * as Cause from "effect/Cause" import * as Effect from "effect/Effect" import type * as Schema from "effect/Schema" -import type { ActorContext, ActorRef, Enqueue, InitialEvent as MachineInitialEvent, Machine } from "../../Machine.js" +import type { + Enqueue, + InitialEvent as MachineInitialEvent, + Machine, + MachineReferences, + MachineTarget +} from "../../Machine.js" import { getTargetBuilder, makeCollector, type RuntimeCommand } from "./command.js" import { type ActiveConfiguration, @@ -94,35 +100,35 @@ export type TransitionHandler ) => Machine.HandlerResult -const rootActorScopes = new WeakMap>() -const machineActorScopes = new WeakMap>() +const rootMachineReferences = new WeakMap>() +const scopedMachineReferences = new WeakMap>() -export const withActorScope = ( +export const withMachineReferences = ( machine: Machine.Any, - actorScope: ActorContext + machineReferences: MachineReferences ): Machine.Any => { const scoped = Object.create(machine) as Machine.Any - machineActorScopes.set(scoped, { self: actorScope.self, parent: actorScope.parent }) + scopedMachineReferences.set(scoped, { self: machineReferences.self, parent: machineReferences.parent }) return scoped } -const getActorContext = ( +const resolveMachineReferences = ( machine: Machine.Any, configuration: ActiveConfiguration -): ActorContext => { - const scope = configuration.actorScope +): MachineReferences => { + const scope = configuration.machineReferences if (scope !== undefined) return scope - const machineScope = machineActorScopes.get(machine) + const machineScope = scopedMachineReferences.get(machine) if (machineScope !== undefined) return machineScope - const cached = rootActorScopes.get(machine) + const cached = rootMachineReferences.get(machine) if (cached !== undefined) return cached - const self: ActorRef = { + const self: MachineTarget = { id: machine.id ?? "Machine", sessionId: "Machine.plan", send: () => Effect.fail(new StoppedError()) } const root = { self, parent: undefined } - rootActorScopes.set(machine, root) + rootMachineReferences.set(machine, root) return root } @@ -240,7 +246,7 @@ const completeHistoryConfiguration = ( values, outputs: configuration.outputs, history: configuration.history, - actorScope: configuration.actorScope + machineReferences: configuration.machineReferences } as ActiveConfiguration active.add(child.path) if (child.schema !== undefined) { @@ -249,7 +255,7 @@ const completeHistoryConfiguration = ( throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`) } const initialized = collectStateInitializer(machine, initializer, { - ...getActorContext(machine, current), + ...resolveMachineReferences(machine, current), state: current.values.get(path), containingState: getParentValue(machine, current, path), ancestors: getParentValues(machine, current, path), @@ -271,14 +277,14 @@ const completeHistoryConfiguration = ( values, outputs: configuration.outputs, history: configuration.history, - actorScope: configuration.actorScope + machineReferences: configuration.machineReferences } as ActiveConfiguration const initializer = valuedMissing.length === 0 ? undefined : machine.handlers[path]?.initial if (valuedMissing.length > 0 && initializer === undefined) { throw new Error(`Machine shallow history requires an initial value implementation for state "${path}"`) } const initialized = initializer === undefined ? undefined : collectStateInitializer(machine, initializer, { - ...getActorContext(machine, current), + ...resolveMachineReferences(machine, current), state: current.values.get(path), containingState: getParentValue(machine, current, path), ancestors: getParentValues(machine, current, path), @@ -542,7 +548,7 @@ const makeStateActionContext = < path: string, event: Machine.LifecycleEvent ): Machine.StateActionContext => ({ - ...getActorContext(machine, configuration), + ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier, containingState: getParentValue(machine, configuration, path) as Machine.ParentStateValue, ancestors: getParentValues(machine, configuration, path) as Machine.ParentStateValues, @@ -562,7 +568,7 @@ const makeTransitionContext = < event: Machine.EventByTag, snapshot: Machine.Snapshot ): Machine.HandlerContext => ({ - ...getActorContext(machine, configuration), + ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier, containingState: getParentValue(machine, configuration, path) as Machine.ParentStateValue, ancestors: getParentValues(machine, configuration, path) as Machine.ParentStateValues, @@ -584,7 +590,7 @@ const makeDoneContext = < output: unknown, snapshot: Machine.Snapshot ): Machine.DoneContext => ({ - ...getActorContext(machine, configuration), + ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier, containingState: getParentValue(machine, configuration, path) as Machine.ParentStateValue, ancestors: getParentValues(machine, configuration, path) as Machine.ParentStateValues, @@ -679,7 +685,7 @@ const selectAlwaysTransitions = < Machine.AlwaysContext> >, context: { - ...getActorContext(machine, configuration), + ...resolveMachineReferences(machine, configuration), state: configuration.values.get(path) as Machine.StateByIdentifier< States, Machine.StateIdentifier @@ -867,7 +873,7 @@ const selectInvocationTransition = < if (transition === undefined) return [] const snapshot = snapshotFromConfiguration(machine, configuration) const context = { - ...getActorContext(machine, configuration), + ...resolveMachineReferences(machine, configuration), state: configuration.values.get(event.path), containingState: getParentValue(machine, configuration, event.path), ancestors: getParentValues(machine, configuration, event.path), @@ -1107,10 +1113,10 @@ const resolveChoiceTarget = ( ]), outputs: configuration.outputs, history: configuration.history, - ...(configuration.actorScope === undefined ? {} : { actorScope: configuration.actorScope }) + ...(configuration.machineReferences === undefined ? {} : { machineReferences: configuration.machineReferences }) } const collected = collectTransition(machine, choice.transition, { - ...getActorContext(machine, provisional), + ...resolveMachineReferences(machine, provisional), containingState: getParentValue(machine, provisional, node.path), ancestors: getParentValues(machine, provisional, node.path), event, diff --git a/src/internal/machine/process.ts b/src/internal/machine/process.ts index fc4e46f..8b83f71 100644 --- a/src/internal/machine/process.ts +++ b/src/internal/machine/process.ts @@ -459,7 +459,7 @@ const makeProcessLogic: < ) => compiledInitial === undefined ? internalRuntime.provideMachineRuntime( - internalPlanner.planInitial(internalPlanner.withActorScope(machine, scope), ...initialArgs).pipe( + internalPlanner.planInitial(internalPlanner.withMachineReferences(machine, scope), ...initialArgs).pipe( Effect.flatMap((planned) => { const commands = planned.commands.length === 0 ? undefined @@ -540,7 +540,7 @@ const makeProcessLogic: < try { planned = internalPlanner.planConfiguration( machine, - Configuration.withActorScope( + Configuration.withMachineReferences( configuration ?? Configuration.normalizeConfigurationSync(machine, current), context ), @@ -651,7 +651,7 @@ const makeProcessLogic: < try { planned = internalPlanner.planConfiguration( machine, - Configuration.withActorScope( + Configuration.withMachineReferences( configuration ?? Configuration.normalizeConfigurationSync(machine, current), context ), diff --git a/src/internal/machine/runtime.ts b/src/internal/machine/runtime.ts index f6bde59..08b12db 100644 --- a/src/internal/machine/runtime.ts +++ b/src/internal/machine/runtime.ts @@ -18,7 +18,7 @@ import * as Scope from "effect/Scope" import * as Stream from "effect/Stream" import * as SynchronizedRef from "effect/SynchronizedRef" import type * as Take from "effect/Take" -import type { ActorRef } from "../../Machine.js" +import type { MachineTarget } from "../../Machine.js" import { ChildAlreadyExistsError, StoppedError } from "./errors.js" type ChildDescriptor = { @@ -325,7 +325,7 @@ interface ProcessAddress { readonly send: (event: Event) => Effect.Effect } -const isProcessAddress = (value: unknown): value is ActorRef => +const isProcessAddress = (value: unknown): value is MachineTarget => typeof value === "object" && value !== null && "send" in value && typeof value.send === "function" export interface ProcessScope { @@ -335,7 +335,7 @@ export interface ProcessScope { readonly sendParent: (event: unknown) => Effect.Effect readonly emit: (event: unknown) => Effect.Effect readonly sendTo: { - (target: ActorRef, event: TargetEvent): Effect.Effect + (target: MachineTarget, event: TargetEvent): Effect.Effect (child: ChildSelector, event: unknown): Effect.Effect } readonly stopChild: (child: ChildSelector) => Effect.Effect @@ -367,7 +367,7 @@ export interface ProcessContext extends ProcessScope { * * Unlike `ProcessContext`, synchronous mailbox and state operations do not * introduce an Effect boundary. The compiled drain still returns an Effect so - * actor commands, invokes, observation callbacks, interruption, and the Effect + * machine commands, invokes, observation callbacks, interruption, and the Effect * scheduler remain explicit at their actual boundaries. * * @internal diff --git a/test/internal/machine/strategyDifferential.test.ts b/test/internal/machine/strategyDifferential.test.ts index 7255fd6..812977e 100644 --- a/test/internal/machine/strategyDifferential.test.ts +++ b/test/internal/machine/strategyDifferential.test.ts @@ -600,7 +600,7 @@ describe("machine planner and runtime strategies", () => { initial: "active", run: ({ parent, sendTo, setState }) => parent === undefined ? - Effect.die("worker expected an owning actor") : + Effect.die("worker expected an owning machine") : (current === 1 ? Deferred.succeed(firstStarted, undefined) : Effect.void).pipe( Effect.andThen(Effect.never), Effect.onInterrupt(() => diff --git a/test/machine/ActorEvents.test.ts b/test/machine/MachineReferences.test.ts similarity index 98% rename from test/machine/ActorEvents.test.ts rename to test/machine/MachineReferences.test.ts index 80c138b..2a30868 100644 --- a/test/machine/ActorEvents.test.ts +++ b/test/machine/MachineReferences.test.ts @@ -5,7 +5,7 @@ import { Machine } from "../../src/index.js" const collectNext = (stream: Stream.Stream) => stream.pipe(Stream.take(1), Stream.runCollect, Effect.map(Array.from), Effect.forkChild({ startImmediately: true })) -describe("actor event channels", () => { +describe("machine reference event channels", () => { it.effect("observes initial emissions through a prepared machine", () => Effect.gen(function*() { class Idle extends Schema.TaggedClass("PreparedEmissionIdle")("Idle", {}) {} @@ -144,7 +144,7 @@ describe("actor event channels", () => { assert.deepStrictEqual(Array.from(yield* Stream.runCollect(ref.emissions)), []) })) - it.effect("fails the actor with a typed machine error when an emission cannot be decoded", () => + it.effect("fails the machine with a typed error when an emission cannot be decoded", () => Effect.gen(function*() { class Idle extends Schema.TaggedClass("InvalidEmissionIdle")("Idle", {}) {} class Publish extends Schema.TaggedClass("InvalidEmissionPublish")("Publish", {}) {} diff --git a/test/machine/support/activityLifecycleModel.ts b/test/machine/support/activityLifecycleModel.ts index 754ab69..8b44e43 100644 --- a/test/machine/support/activityLifecycleModel.ts +++ b/test/machine/support/activityLifecycleModel.ts @@ -83,7 +83,7 @@ export const makeActivityProbe: Effect.Effect = Effect.gen(functi Machine.logic({ initial: () => initial(owner), run: ({ parent, sendTo, state }) => - parent === undefined ? Effect.die("activity expected an owning actor") : state.pipe( + parent === undefined ? Effect.die("activity expected an owning machine") : state.pipe( Effect.flatMap(({ epoch, release }) => { switch (behavior._tag) { case "Blocked": diff --git a/test/unstable/cluster/ClusterMachine.test.ts b/test/unstable/cluster/ClusterMachine.test.ts index f15e7e1..1296a90 100644 --- a/test/unstable/cluster/ClusterMachine.test.ts +++ b/test/unstable/cluster/ClusterMachine.test.ts @@ -540,7 +540,7 @@ describe("ClusterMachine", () => { }).pipe(Effect.provide(makeLayer(bridge, storage.service, () => Effect.void))) })) - it.effect("rejects process-local actor commands", () => + it.effect("rejects process-local machine commands", () => Effect.gen(function*() { const gate = yield* Latch.make() const state = { gate, initialEntries: 0, actions: 0, inFlight: 0, maxInFlight: 0 } diff --git a/typetest/machine/ActorEvents.tst.ts b/typetest/machine/MachineReferences.tst.ts similarity index 85% rename from typetest/machine/ActorEvents.tst.ts rename to typetest/machine/MachineReferences.tst.ts index a2cad3f..2b5d1d7 100644 --- a/typetest/machine/ActorEvents.tst.ts +++ b/typetest/machine/MachineReferences.tst.ts @@ -4,19 +4,19 @@ import { describe, expect, it } from "tstyche" import { Machine } from "../../src/index.js" import { AtomMachine } from "../../src/unstable/reactivity/index.js" -describe("machine actor event channels", () => { - class Idle extends Schema.TaggedClass("ActorEventsIdle")("Idle", {}) {} - class Ping extends Schema.TaggedClass("ActorEventsPing")("Ping", {}) {} - class Local extends Schema.TaggedClass("ActorEventsLocal")("Local", {}) {} - class ParentNotice extends Schema.TaggedClass("ActorEventsParentNotice")("ParentNotice", { +describe("machine reference event channels", () => { + class Idle extends Schema.TaggedClass("MachineReferencesIdle")("Idle", {}) {} + class Ping extends Schema.TaggedClass("MachineReferencesPing")("Ping", {}) {} + class Local extends Schema.TaggedClass("MachineReferencesLocal")("Local", {}) {} + class ParentNotice extends Schema.TaggedClass("MachineReferencesParentNotice")("ParentNotice", { value: Schema.Number }) {} - class OtherParentEvent extends Schema.TaggedClass("ActorEventsOtherParent")( + class OtherParentEvent extends Schema.TaggedClass("MachineReferencesOtherParent")( "OtherParentEvent", {} ) {} - class Published extends Schema.TaggedClass("ActorEventsPublished")("Published", {}) {} - class ValuedPublished extends Schema.TaggedClass("ActorEventsValuedPublished")( + class Published extends Schema.TaggedClass("MachineReferencesPublished")("Published", {}) {} + class ValuedPublished extends Schema.TaggedClass("MachineReferencesValuedPublished")( "ValuedPublished", { value: Schema.Number } ) {} @@ -43,6 +43,14 @@ describe("machine actor event channels", () => { const Child = Machine.child("child", childMachine) it("types self, parent, raised events, and emissions as separate channels", () => { + expect["send"]>().type.toBeCallableWith(new Ping({})) + expect["self"]>().type.toBe< + Machine.MachineTarget> + >() + expect< + Machine.MachineReferences["parent"] + >().type.toBe> | undefined>() + Machine.make({ states: states.states, events: Events,