effect-machine 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,9 @@
2
2
 
3
3
  Type-safe state machines for [Effect](https://effect.website).
4
4
 
5
- Complex workflows usually fail the same way: one `status` field, a few side booleans, and effects scattered across callbacks. `effect-machine` gives you one typed model for state, events, and transitions, then runs it as a real actor.
5
+ Effect Machine gives one actor a schema-first state model, a typed event mailbox, scoped Effect work, typed input and output, supervision, persistence hooks, inspection, and framework-neutral Atom integration.
6
6
 
7
- Use it when a feature has:
8
-
9
- - multiple valid and invalid states
10
- - async work tied to state entry
11
- - retries, timeouts, cancellation, or backpressure
12
- - logic you want to reuse in-process, in tests, and in distributed systems
7
+ Use it when a feature has several valid states, invalid transitions, state-owned async work, timeouts, cancellation, actor coordination, or UI views that need precise subscriptions.
13
8
 
14
9
  ## Install
15
10
 
@@ -17,197 +12,247 @@ Use it when a feature has:
17
12
  bun add effect-machine effect
18
13
  ```
19
14
 
20
- `effect` is a peer dependency. The repository validates the package with
21
- `@effect/tsgo`, the latest Effect release candidate, type-aware oxlint, and Bun tests.
15
+ `effect` is a required peer dependency.
16
+
17
+ ## Imports
18
+
19
+ ```ts
20
+ import { Event, Machine, State } from "effect-machine";
21
+ import * as ActorAtom from "effect-machine/atom";
22
+ import { EntityMachine, toEntity } from "effect-machine/cluster";
23
+ ```
24
+
25
+ Use `effect-machine` for local machines and actors. Use `effect-machine/atom` for React, Solid, or another Effect Atom binding. Use `effect-machine/cluster` for distributed entity machines.
22
26
 
23
- ## Core Pattern
27
+ ## First machine
24
28
 
25
- States and events are schemas. Types, validation, and serialization from one place.
29
+ States and events are Effect schemas.
26
30
 
27
31
  ```ts
28
- import { Cause, Context, Effect, Schema } from "effect";
32
+ import { Effect, Schema } from "effect";
29
33
  import { Event, Machine, State } from "effect-machine";
30
34
 
31
- const CheckoutState = State({
32
- ReviewingCart: { cartId: Schema.String, totalCents: Schema.Number },
33
- ChargingCard: { cartId: Schema.String, totalCents: Schema.Number },
34
- Confirmed: { cartId: Schema.String, receiptId: Schema.String },
35
- Failed: { cartId: Schema.String, reason: Schema.String },
35
+ const DownloadState = State({
36
+ Idle: {},
37
+ Downloading: { url: Schema.String },
38
+ Done: { url: Schema.String, bytes: Schema.Finite },
39
+ Failed: { url: Schema.String, message: Schema.String },
36
40
  });
37
41
 
38
- const CheckoutEvent = Event({
39
- Submit: {},
40
- Charged: { receiptId: Schema.String },
41
- Declined: { reason: Schema.String },
42
- Cancel: {},
42
+ const DownloadEvent = Event({
43
+ Start: { url: Schema.String },
44
+ Completed: { bytes: Schema.Finite },
45
+ Failed: { message: Schema.String },
43
46
  });
44
47
 
45
- class PaymentService extends Context.Service<
46
- PaymentService,
47
- {
48
- readonly chargeCard: (
49
- cartId: string,
50
- totalCents: number,
51
- ) => Effect.Effect<{ readonly receiptId: string }>;
52
- }
53
- >()("app/PaymentService") {}
54
-
55
- const checkoutMachine = Machine.make({
56
- state: CheckoutState,
57
- event: CheckoutEvent,
58
- initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
48
+ const downloadMachine = Machine.make({
49
+ state: DownloadState,
50
+ event: DownloadEvent,
51
+ initial: DownloadState.Idle,
59
52
  })
60
- .on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
61
- CheckoutState.ChargingCard.with(state),
53
+ .on(DownloadState.Idle, DownloadEvent.Start, ({ event }) =>
54
+ DownloadState.Downloading({ url: event.url }),
62
55
  )
63
- .on(CheckoutState.ChargingCard, CheckoutEvent.Charged, ({ state, event }) =>
64
- CheckoutState.Confirmed.with(state, { receiptId: event.receiptId }),
56
+ .on(DownloadState.Downloading, DownloadEvent.Completed, ({ state, event }) =>
57
+ DownloadState.Done.with(state, { bytes: event.bytes }),
65
58
  )
66
- .on(CheckoutState.ChargingCard, CheckoutEvent.Declined, ({ state, event }) =>
67
- CheckoutState.Failed.with(state, { reason: event.reason }),
68
- )
69
- .onAny(CheckoutEvent.Cancel, ({ state }) =>
70
- CheckoutState.Failed.with(state, { reason: "cancelled" }),
71
- )
72
- .task(
73
- CheckoutState.ChargingCard,
74
- ({ state }) =>
75
- Effect.flatMap(PaymentService, (payment) =>
76
- payment.chargeCard(state.cartId, state.totalCents),
77
- ),
78
- {
79
- onSuccess: ({ receiptId }) => CheckoutEvent.Charged({ receiptId }),
80
- onFailure: (cause) => CheckoutEvent.Declined({ reason: Cause.pretty(cause) }),
81
- },
59
+ .on(DownloadState.Downloading, DownloadEvent.Failed, ({ state, event }) =>
60
+ DownloadState.Failed.with(state, { message: event.message }),
82
61
  )
83
- .final(CheckoutState.Confirmed)
84
- .final(CheckoutState.Failed);
62
+ .final(DownloadState.Done, ({ state }) => state.bytes)
63
+ .final(DownloadState.Failed, () => 0);
64
+
65
+ const program = Effect.scoped(
66
+ Machine.scoped(
67
+ Effect.gen(function* () {
68
+ const actor = yield* Machine.spawn(downloadMachine);
69
+ yield* actor.start;
70
+ yield* actor.send(DownloadEvent.Start({ url: "/report.pdf" }));
71
+ yield* actor.send(DownloadEvent.Completed({ bytes: 1024 }));
72
+ return yield* actor.awaitOutput;
73
+ }),
74
+ ),
75
+ );
85
76
  ```
86
77
 
87
- A few things to notice:
78
+ An empty variant is a value such as `DownloadState.Idle`. A non-empty variant is a constructor such as `DownloadState.Downloading({ url })`.
79
+
80
+ `State.with(source, fields)` copies matching fields into the target variant. It prevents manual context spreading across different states.
81
+
82
+ ## Effect is the composition layer
83
+
84
+ Effect Machine does not add an action queue or a second context system.
85
+
86
+ | Work | API |
87
+ | ------------------------------------- | -------------------------------------------- |
88
+ | Unconditional state change | `.on`, `.reenter`, or `.immediate` |
89
+ | Conditional state change | `.when`, `.reenterWhen`, or `.immediateWhen` |
90
+ | Work that produces a completion event | `.task` |
91
+ | State-owned stream or resource | `.spawn` |
92
+ | Actor-owned stream or resource | `.background` |
93
+ | Autonomous machine sequence | `Machine.run` with `Effect.flatMap` |
94
+ | Interactive multi-phase flow | Parent machine with child actors |
88
95
 
89
- - Empty variants are values: `State.Idle`. Non-empty are constructors: `State.Loading({ url })`.
90
- - `State.with(source, overrides)` carries overlapping fields forward without manual copying.
91
- - `.onAny(...)` is a fallback; a specific `.on(...)` wins.
92
- - `.spawn(...)` runs work on state entry and cancels it on state exit.
96
+ Effect requirements remain in `R`. A machine cannot start until the application provides every required service. Effectful transition handlers must have `never` in their error channel. Convert expected failures to states or events.
93
97
 
94
- The builder also supports `.timeout(state, { duration, event })`, `.postpone(state, event)` for buffering, and `.reenter(...)` for re-running lifecycle on same-state transitions.
98
+ Read [the Effect model](./docs/effect-model.md) and [async work ownership](./docs/async-work.md).
95
99
 
96
- ## Effect Services
100
+ ## Guards and stable state
97
101
 
98
- Task, spawn, and background handlers can use standard Effect services. The machine type records each service requirement.
102
+ Register ordered candidates for one state and event. The first passing guard wins. An unguarded candidate is the fallback.
99
103
 
100
104
  ```ts
101
- const actor =
102
- yield *
103
- Machine.spawn(checkoutMachine).pipe(
104
- Effect.provideService(PaymentService, {
105
- chargeCard: (cartId) => Effect.succeed({ receiptId: `rcpt_${cartId}` }),
106
- }),
107
- );
108
- yield * actor.start;
105
+ machine
106
+ .when(
107
+ State.Checking,
108
+ Event.Continue,
109
+ function hasStock({ state }) {
110
+ return state.stock > 0;
111
+ },
112
+ () => State.Accepted,
113
+ )
114
+ .on(State.Checking, Event.Continue, () => State.Rejected)
115
+ .immediate(State.Accepted, ({ state }) => State.Ready.with(state));
109
116
  ```
110
117
 
111
- `Machine.spawn` captures the current Effect context. A later `actor.start` keeps those services. Use a different layer or service value in each test or runtime.
118
+ The predicate can return a Boolean or `Effect<boolean, never, R>`. Its requirements flow into the machine type. `actor.can(event)` evaluates the same predicate with the actor's captured context. `ActorAtom.can(actor, event)` exposes the result to React and Solid. The inspector uses the predicate function name.
112
119
 
113
- Transition handlers in `.on()` and `.reenter()` stay pure. Use services only in `.task()`, `.spawn()`, and `.background()`.
120
+ Immediate transitions run until the state is stable. Subscribers see only the stable state. The runtime stops an accidental eventless loop after 100 edges.
114
121
 
115
- ## Running Actors
116
-
117
- `Machine.spawn` allocates an actor but does not start it. Call `actor.start` to fork the event loop, background effects, and spawn effects. Events sent before `start()` are queued.
122
+ ## Effect services and tasks
118
123
 
119
124
  ```ts
120
- const program = Effect.gen(function* () {
121
- const actor = yield* Machine.spawn(checkoutMachine);
122
- yield* actor.start;
123
-
124
- yield* actor.send(CheckoutEvent.Submit);
125
- const finalState = yield* actor.awaitFinal;
125
+ class Api extends Context.Service<
126
+ Api,
127
+ { readonly load: (id: string) => Effect.Effect<Data, ApiError> }
128
+ >()("app/Api") {}
129
+
130
+ machine.task(State.Loading, ({ state }) => Effect.flatMap(Api, (api) => api.load(state.id)), {
131
+ name: "load-data",
132
+ onSuccess: (data) => Event.Loaded({ data }),
133
+ onFailure: (error) => Event.LoadFailed({ message: String(error) }),
126
134
  });
127
135
 
128
- Effect.runPromise(
129
- Effect.scoped(program).pipe(
130
- Effect.provideService(PaymentService, {
131
- chargeCard: (cartId) => Effect.succeed({ receiptId: `rcpt_${cartId}` }),
132
- }),
133
- ),
134
- );
136
+ const actor = yield * Machine.spawn(machine).pipe(Effect.provide(ApiLive));
137
+ yield * actor.start;
135
138
  ```
136
139
 
137
- Key actor operations:
140
+ The actor captures the Effect context during allocation. It keeps those services when it starts later.
138
141
 
139
- - `start` forks the event loop (idempotent, required after `Machine.spawn`)
140
- - `send(event)` queues and returns immediately
141
- - `call(event)` returns full transition info
142
- - `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
143
- - `waitFor(...)` / `awaitFinal` for coordination
144
- - `stop` interrupts now; `drain` processes the remaining queue first
145
- - `awaitExit` completes when the actor stops
142
+ `onFailure` receives the typed Effect error. A defect does not enter `onFailure`. It stops the actor or starts supervision.
146
143
 
147
- For named actors or shared lookup, use an actor system. `system.spawn` auto-starts — no `actor.start` needed:
144
+ ## Input, output, and composition
148
145
 
149
146
  ```ts
150
- import { ActorSystemDefault, ActorSystemService } from "effect-machine";
147
+ const checkoutMachine = Machine.make({
148
+ state: CheckoutState,
149
+ event: CheckoutEvent,
150
+ initial: (input: CheckoutInput) => CheckoutState.Reviewing(input),
151
+ }).final(CheckoutState.Done, ({ state }) => ({ receiptId: state.receiptId }));
151
152
 
152
- const program = Effect.gen(function* () {
153
- const system = yield* ActorSystemService;
154
- const actor = yield* system.spawn("checkout-123", checkoutMachine);
155
- yield* actor.send(CheckoutEvent.Submit);
156
- }).pipe(Effect.provide(ActorSystemDefault));
153
+ const program = Machine.run(cartMachine).pipe(
154
+ Effect.flatMap((cart) => Machine.run(checkoutMachine, { input: cart })),
155
+ );
157
156
  ```
158
157
 
159
- ### Typed Replies
158
+ `Machine.run` starts one actor, waits for output, and always stops it. Interruption releases actor resources. The final actor state remains available when you use `Machine.spawn` and retain the actor reference.
160
159
 
161
- Events can declare typed reply schemas:
160
+ Use a parent machine when a UI must route between phases, keep shared values, or support back navigation. Read [Actors and systems](./docs/actors.md).
162
161
 
163
- ```ts
164
- const CartEvent = Event({
165
- GetTotal: Event.reply({}, Schema.Number),
166
- });
162
+ ## ActorRef
163
+
164
+ | Member | Use |
165
+ | --------------------------- | -------------------------------------------------- |
166
+ | `start` | Start a direct actor |
167
+ | `send(event)` | Queue an event |
168
+ | `call(event)` | Process an event and return transition information |
169
+ | `ask(event)` | Return a typed reply from an `Event.reply` event |
170
+ | `waitFor(state)` | Wait for a state constructor or predicate |
171
+ | `sendAndWait(event, state)` | Send and wait for a state |
172
+ | `snapshot` | Read the current state as an Effect |
173
+ | `awaitFinal` | Wait for the final state |
174
+ | `awaitOutput` | Wait for typed output |
175
+ | `awaitExit` | Wait for `Final`, `Stopped`, or `Defect` |
176
+ | `drain` | Process queued events and stop |
177
+ | `subscribe` | Observe state with a host callback |
178
+ | `client` | Use the actor outside Effect |
179
+ | `system` | Access named actors |
180
+ | `children` | Read direct child actors |
181
+
182
+ `Machine.spawn` returns an unstarted actor. `system.spawn` starts the actor.
167
183
 
168
- machine.on(State.Active, CartEvent.GetTotal, ({ state }) => Machine.reply(state, state.totalCents));
184
+ Use `actor.client` in a JavaScript callback or application that does not run inside Effect. `client.can(event)` returns a Promise and supports Effect predicates. `client.canSync(event)` supports Boolean predicates only. React and Solid should use Actor Atoms.
185
+
186
+ ## Atom, React, and Solid
187
+
188
+ ```ts
189
+ import * as ActorAtom from "effect-machine/atom";
169
190
 
170
- const total = yield * actor.ask(CartEvent.GetTotal); // number
191
+ const stateAtom = ActorAtom.make(actor);
192
+ const countAtom = ActorAtom.select(stateAtom, (state) => state.count);
171
193
  ```
172
194
 
195
+ The selected Atom stays writable. Writes send machine events. A selector publishes only when its selected value changes.
196
+
197
+ The React example uses `useAtomSuspense` and Motion. The Solid example uses `useAtomResource`, Suspense, and `solid-transition-group`. Both include performance tests. Both retain exit-animation data in the terminal machine state.
198
+
199
+ Read [Atom and UI integration](./docs/atom-and-ui.md) and browse [all examples](./examples/README.md).
200
+
201
+ ## Persistence, supervision, and inspection
202
+
203
+ - Recovery resolves state during actor startup.
204
+ - Durability saves committed transitions.
205
+ - Supervision restarts defects within an Effect `Schedule` budget.
206
+ - Inspection reports events, named transition operations, transitions, named guards, tasks, Effects, errors, stops, and actor generations.
207
+
208
+ Read [Persistence and supervision](./docs/persistence-and-supervision.md) and [Inspection](./docs/inspection.md).
209
+
173
210
  ## Testing
174
211
 
175
- Test transitions without spawning actors:
212
+ Use `simulate` or `createTestHarness` for transition paths. Spawn a real actor for tasks, services, resources, persistence, supervision, inspection, and actor topology.
176
213
 
177
214
  ```ts
178
- import { simulate } from "effect-machine";
215
+ const result = yield * simulate(machine, events, { input });
216
+ yield * assertPath(machine, events, ["Idle", "Loading", "Done"]);
217
+ yield * assertNeverReaches(machine, events, "Failed");
218
+ ```
179
219
 
180
- const result =
181
- yield *
182
- simulate(checkoutMachine, [
183
- CheckoutEvent.Submit,
184
- CheckoutEvent.Charged({ receiptId: "rcpt_123" }),
185
- ]);
220
+ Read [Testing](./docs/testing.md).
186
221
 
187
- expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard", "Confirmed"]);
188
- ```
222
+ ## XState migration
189
223
 
190
- `simulate` and `createTestHarness` test transition logic. They do not run `.spawn()` or `.background()` effects.
224
+ The [migration guide](./docs/xstate-migration.md) covers context, assign, actions, invoked promise and callback actors, root routers, actor registries, selectors, inspection, persistence, and exit animation values. Its patterns come from a large XState kiosk application.
191
225
 
192
- ## Cluster
226
+ ## Cluster entities
193
227
 
194
- When the same machine needs to run behind `@effect/cluster`, turn it into an entity:
228
+ Use `effect-machine/cluster` to expose a machine through Effect Cluster. It supports typed send, ask, state reads, state watches, input adapters, snapshot persistence, and journal persistence.
195
229
 
196
- ```ts
197
- import { EntityMachine, toEntity } from "effect-machine/cluster";
230
+ Read [Cluster entities](./docs/cluster.md).
198
231
 
199
- const CheckoutEntity = toEntity(checkoutMachine, { type: "Checkout" });
232
+ ## Examples
200
233
 
201
- const CheckoutEntityLayer = EntityMachine.layer(CheckoutEntity, checkoutMachine, {
202
- initializeState: (entityId) => CheckoutState.ReviewingCart({ cartId: entityId, totalCents: 0 }),
203
- persistence: { strategy: "journal" },
204
- });
234
+ The examples directory is a Bun workspace.
235
+
236
+ ```bash
237
+ bun run examples:gate
238
+ bun run example:react
239
+ bun run example:solid
205
240
  ```
206
241
 
207
- Persistence strategies:
242
+ The [example matrix](./examples/README.md) links every pattern to executable code.
243
+
244
+ ## Documentation
208
245
 
209
- - **Snapshot** — saves state periodically, restores on reactivation
210
- - **Journal** — appends events on each RPC, replays on reactivation
246
+ - [Effect model](./docs/effect-model.md)
247
+ - [Async work](./docs/async-work.md)
248
+ - [Actors and systems](./docs/actors.md)
249
+ - [Atom and UI integration](./docs/atom-and-ui.md)
250
+ - [Persistence and supervision](./docs/persistence-and-supervision.md)
251
+ - [Inspection](./docs/inspection.md)
252
+ - [Testing](./docs/testing.md)
253
+ - [Migration from XState](./docs/xstate-migration.md)
254
+ - [Cluster entities](./docs/cluster.md)
255
+ - [AI agent reference](./SKILL.md)
211
256
 
212
257
  ## License
213
258
 
package/dist/actor.d.ts CHANGED
@@ -5,17 +5,31 @@ import { Lifecycle, Machine } from "./machine.js";
5
5
  import { ProcessEventResult } from "./internal/transition.js";
6
6
  import { Context, Effect, Layer, Option, Scope, Stream, SubscriptionRef } from "effect";
7
7
  //#region src/actor.d.ts
8
- /**
9
- * Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
10
- */
8
+ /** JavaScript client for code that does not run inside Effect. */
9
+ interface ActorClient<State extends {
10
+ readonly _tag: string;
11
+ }, Event, Output = State> {
12
+ readonly send: (event: Event) => void;
13
+ readonly stop: () => void;
14
+ readonly getSnapshot: () => State;
15
+ readonly matches: (tag: State["_tag"]) => boolean;
16
+ readonly canSync: (event: Event) => boolean;
17
+ readonly can: (event: Event) => Promise<boolean>;
18
+ readonly getLifecycle: () => ActorLifecycle<State, Output>;
19
+ readonly getLatestTransition: () => TransitionInfo<State, Event> | undefined;
20
+ readonly subscribe: (listener: (state: State) => void) => () => void;
21
+ }
22
+ /** @deprecated Use `ActorClient`. */
11
23
  interface ActorRefSync<State extends {
12
24
  readonly _tag: string;
13
- }, Event> {
25
+ }, Event, Output = State> {
14
26
  readonly send: (event: Event) => void;
15
27
  readonly stop: () => void;
16
28
  readonly snapshot: () => State;
17
29
  readonly matches: (tag: State["_tag"]) => boolean;
18
30
  readonly can: (event: Event) => boolean;
31
+ readonly lifecycle: () => ActorLifecycle<State, Output>;
32
+ readonly latestTransition: () => TransitionInfo<State, Event> | undefined;
19
33
  }
20
34
  /**
21
35
  * Information about a successful transition.
@@ -26,9 +40,19 @@ interface TransitionInfo<State, Event> {
26
40
  readonly toState: State;
27
41
  readonly event: Event;
28
42
  }
43
+ /** Observable actor lifecycle. Domain state remains available through `actor.state`. */
44
+ type ActorLifecycle<State, Output = State> = {
45
+ readonly _tag: "Created";
46
+ } | {
47
+ readonly _tag: "Starting";
48
+ readonly generation: number;
49
+ } | {
50
+ readonly _tag: "Active";
51
+ readonly generation: number;
52
+ } | ActorExit<State, Output>;
29
53
  interface ActorRef<State extends {
30
54
  readonly _tag: string;
31
- }, Event> {
55
+ }, Event, Output = State> {
32
56
  readonly id: string;
33
57
  /** Send an event (fire-and-forget). */
34
58
  readonly send: (event: Event) => Effect.Effect<void>;
@@ -36,7 +60,7 @@ interface ActorRef<State extends {
36
60
  * Serialized request-reply (OTP gen_server:call).
37
61
  * Event is processed through the queue; caller gets ProcessEventResult back.
38
62
  */
39
- readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
63
+ readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State, Event>>;
40
64
  /**
41
65
  * Typed request-reply. Accepts only events with a reply schema
42
66
  * (defined via `Event.reply()`). Return type is inferred from the schema.
@@ -45,6 +69,10 @@ interface ActorRef<State extends {
45
69
  readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError | ActorStoppedError>;
46
70
  /** Observable state. */
47
71
  readonly state: SubscriptionRef.SubscriptionRef<State>;
72
+ /** Observable actor lifecycle. */
73
+ readonly lifecycle: SubscriptionRef.SubscriptionRef<ActorLifecycle<State, Output>>;
74
+ /** The latest accepted edge. This value remains available after actor exit. */
75
+ readonly latestTransition: SubscriptionRef.SubscriptionRef<TransitionInfo<State, Event> | undefined>;
48
76
  /** Stop the actor gracefully. */
49
77
  readonly stop: Effect.Effect<void>;
50
78
  /**
@@ -60,7 +88,7 @@ interface ActorRef<State extends {
60
88
  readonly snapshot: Effect.Effect<State>;
61
89
  /** Check if current state matches tag. */
62
90
  readonly matches: (tag: State["_tag"]) => Effect.Effect<boolean>;
63
- /** Check if event can be handled in current state. */
91
+ /** Check if an event has an enabled transition. Supports Boolean and Effect predicates. */
64
92
  readonly can: (event: Event) => Effect.Effect<boolean>;
65
93
  /** Stream of state changes. */
66
94
  readonly changes: Stream.Stream<State>;
@@ -81,6 +109,8 @@ interface ActorRef<State extends {
81
109
  };
82
110
  /** Wait for a final state (includes current snapshot). */
83
111
  readonly awaitFinal: Effect.Effect<State>;
112
+ /** Wait for the domain output of a final state. */
113
+ readonly awaitOutput: Effect.Effect<Output, ActorStoppedError>;
84
114
  /** Send event and wait for predicate, state variant, or final state. */
85
115
  readonly sendAndWait: {
86
116
  (event: Event, predicate: (state: State) => boolean): Effect.Effect<State>;
@@ -91,18 +121,20 @@ interface ActorRef<State extends {
91
121
  };
92
122
  /** Subscribe to state changes (sync callback). Returns unsubscribe function. */
93
123
  readonly subscribe: (fn: (state: State) => void) => () => void;
124
+ /** JavaScript client for callbacks and applications outside Effect. */
125
+ readonly client: ActorClient<State, Event, Output>;
94
126
  /**
95
127
  * Wait for this actor's terminal exit. Resolves with the exit reason.
96
128
  * Set exactly once when the actor terminates (final, stop, drain, or defect).
97
129
  */
98
- readonly awaitExit: Effect.Effect<ActorExit<State>>;
130
+ readonly awaitExit: Effect.Effect<ActorExit<State, Output>>;
99
131
  /**
100
132
  * Drain: process all remaining events in the queue, then stop.
101
133
  * Unlike `stop` (which interrupts immediately), `drain` lets the actor finish its work.
102
134
  */
103
135
  readonly drain: Effect.Effect<void>;
104
- /** Sync helpers for non-Effect boundaries. */
105
- readonly sync: ActorRefSync<State, Event>;
136
+ /** @deprecated Use `client`. */
137
+ readonly sync: ActorRefSync<State, Event, Output>;
106
138
  /** The actor system this actor belongs to. */
107
139
  readonly system: ActorSystemService;
108
140
  /** Child actors spawned via `self.spawn` in this actor's handlers. */
@@ -147,14 +179,14 @@ interface ActorSystemService {
147
179
  * const actor = yield* system.spawn("my-actor", machine);
148
180
  * ```
149
181
  */
150
- readonly spawn: <S extends {
151
- readonly _tag: string;
152
- }, E extends {
153
- readonly _tag: string;
154
- }, R>(id: string, machine: Machine<S, E, R, any, any>, options?: {
155
- readonly supervision?: Supervision.Policy;
156
- readonly lifecycle?: Lifecycle<S, E>;
157
- }) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
182
+ readonly spawn: {
183
+ <S extends AnyState, E extends {
184
+ readonly _tag: string;
185
+ }, R, Output>(id: string, machine: Machine<S, E, R, any, any, void, Output>, options?: SystemSpawnOptions<S, E, void>): Effect.Effect<ActorRef<S, E, Output>, DuplicateActorError, R>;
186
+ <S extends AnyState, E extends {
187
+ readonly _tag: string;
188
+ }, R, Input, Output>(id: string, machine: Machine<S, E, R, any, any, Input, Output>, options: SystemSpawnOptions<S, E, Input>): Effect.Effect<ActorRef<S, E, Output>, DuplicateActorError, R>;
189
+ };
158
190
  /**
159
191
  * Get an existing actor by ID
160
192
  */
@@ -179,6 +211,15 @@ interface ActorSystemService {
179
211
  */
180
212
  readonly subscribe: (fn: SystemEventListener) => () => void;
181
213
  }
214
+ type SystemSpawnOptions<S, E, Input> = {
215
+ readonly supervision?: Supervision.Policy;
216
+ readonly lifecycle?: Lifecycle<S, E>;
217
+ readonly hydrate?: S;
218
+ } & ([Input] extends [void] ? {
219
+ readonly input?: never;
220
+ } : {
221
+ readonly input: Input;
222
+ });
182
223
  declare const ActorSystem_base: Context.ServiceClass<ActorSystem, "effect-machine/actor/ActorSystem", ActorSystemService>;
183
224
  /**
184
225
  * ActorSystem service tag
@@ -203,16 +244,18 @@ declare const createActor: <S extends {
203
244
  readonly _tag: string;
204
245
  }, E extends {
205
246
  readonly _tag: string;
206
- }, R>(id: string, machine: Machine<S, E, R, any, any>, options?: {
207
- initialState?: S;
247
+ }, R, O>(id: string, machine: Machine<S, E, R, any, any, any, O>, options: {
248
+ initialState: S;
249
+ machineInitial: S;
250
+ hydrated?: boolean;
208
251
  supervision?: Supervision.Policy;
209
252
  lifecycle?: Lifecycle<S, E>;
210
253
  /** @internal Called by system after each restart — emits ActorRestarted system event */
211
254
  onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
212
- } | undefined) => Effect.Effect<ActorRef<S, E>, never, R>;
255
+ }) => Effect.Effect<ActorRef<S, E, O>, never, R>;
213
256
  /**
214
257
  * Default ActorSystem layer
215
258
  */
216
259
  declare const Default: Layer.Layer<ActorSystem, never, never>;
217
260
  //#endregion
218
- export { ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, type ProcessEventResult, SystemEvent, SystemEventListener, TransitionInfo, createActor };
261
+ export { ActorClient, ActorLifecycle, ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, type ProcessEventResult, SystemEvent, SystemEventListener, SystemSpawnOptions, TransitionInfo, createActor };