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
15 changes: 15 additions & 0 deletions .changeset/bright-machines-reference.md
Original file line number Diff line number Diff line change
@@ -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<Event> // before
Machine.MachineTarget<Event> // after

Machine.ActorContext<InputEvents, ParentEvents> // before
Machine.MachineReferences<InputEvents, ParentEvents> // after
```

The inferred `self` and `parent` fields and all runtime behavior are unchanged.
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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<Event>` values. The
shared `Machine.MachineReferences<InputEvents, ParentEvents>` 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

Expand Down Expand Up @@ -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
Expand Down
24 changes: 13 additions & 11 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) => {
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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<Event>` values,
provided by the shared `MachineReferences<InputEvents, ParentEvents>` 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
Expand Down
10 changes: 5 additions & 5 deletions examples/platformer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ let deliver: ((event: SharedEvent) => void) | undefined
const pendingEvents: Array<SharedEvent> = []

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." })
Expand All @@ -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." }))
Expand All @@ -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." })
Expand Down
Loading