effect-machine 0.17.0 → 0.18.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -54
  3. package/dist/actor.d.ts +8 -45
  4. package/dist/actor.js +157 -231
  5. package/dist/cluster/adapters/in-memory.d.ts +0 -1
  6. package/dist/cluster/adapters/in-memory.js +10 -5
  7. package/dist/cluster/entity-actor-ref.d.ts +2 -3
  8. package/dist/cluster/entity-actor-ref.js +14 -17
  9. package/dist/cluster/entity-machine.d.ts +2 -8
  10. package/dist/cluster/entity-machine.js +57 -34
  11. package/dist/cluster/index.js +2 -2
  12. package/dist/cluster/persistence.d.ts +0 -1
  13. package/dist/cluster/to-entity.d.ts +19 -21
  14. package/dist/cluster/to-entity.js +18 -19
  15. package/dist/errors.d.ts +11 -37
  16. package/dist/errors.js +12 -30
  17. package/dist/index.d.ts +4 -5
  18. package/dist/index.js +4 -5
  19. package/dist/inspection.d.ts +0 -1
  20. package/dist/inspection.js +24 -13
  21. package/dist/internal/brands.d.ts +0 -1
  22. package/dist/internal/event-advancement.d.ts +50 -0
  23. package/dist/internal/event-advancement.js +79 -0
  24. package/dist/internal/inspection.d.ts +5 -9
  25. package/dist/internal/inspection.js +31 -10
  26. package/dist/internal/machine-definition.d.ts +16 -0
  27. package/dist/internal/runtime.d.ts +1 -161
  28. package/dist/internal/runtime.js +191 -150
  29. package/dist/internal/transition.d.ts +4 -135
  30. package/dist/internal/transition.js +82 -157
  31. package/dist/internal/utils.d.ts +3 -40
  32. package/dist/internal/utils.js +2 -2
  33. package/dist/machine.d.ts +47 -137
  34. package/dist/machine.js +147 -215
  35. package/dist/schema.d.ts +42 -21
  36. package/dist/schema.js +28 -18
  37. package/dist/supervision.d.ts +1 -24
  38. package/dist/supervision.js +2 -3
  39. package/dist/testing.d.ts +15 -29
  40. package/dist/testing.js +76 -95
  41. package/package.json +19 -38
  42. package/dist/slot.d.ts +0 -159
  43. package/dist/slot.js +0 -165
  44. package/v3/dist/_virtual/_rolldown/runtime.js +0 -13
  45. package/v3/dist/actor.d.ts +0 -250
  46. package/v3/dist/actor.js +0 -577
  47. package/v3/dist/cluster/adapters/in-memory.d.ts +0 -15
  48. package/v3/dist/cluster/adapters/in-memory.js +0 -62
  49. package/v3/dist/cluster/entity-actor-ref.d.ts +0 -49
  50. package/v3/dist/cluster/entity-actor-ref.js +0 -19
  51. package/v3/dist/cluster/entity-machine.d.ts +0 -74
  52. package/v3/dist/cluster/entity-machine.js +0 -166
  53. package/v3/dist/cluster/index.d.ts +0 -6
  54. package/v3/dist/cluster/index.js +0 -6
  55. package/v3/dist/cluster/persistence.d.ts +0 -48
  56. package/v3/dist/cluster/persistence.js +0 -14
  57. package/v3/dist/cluster/to-entity.d.ts +0 -69
  58. package/v3/dist/cluster/to-entity.js +0 -59
  59. package/v3/dist/errors.d.ts +0 -95
  60. package/v3/dist/errors.js +0 -54
  61. package/v3/dist/index.d.ts +0 -11
  62. package/v3/dist/index.js +0 -9
  63. package/v3/dist/inspection.d.ts +0 -151
  64. package/v3/dist/inspection.js +0 -128
  65. package/v3/dist/internal/brands.d.ts +0 -50
  66. package/v3/dist/internal/inspection.d.ts +0 -11
  67. package/v3/dist/internal/inspection.js +0 -20
  68. package/v3/dist/internal/runtime.d.ts +0 -161
  69. package/v3/dist/internal/runtime.js +0 -360
  70. package/v3/dist/internal/transition.d.ts +0 -190
  71. package/v3/dist/internal/transition.js +0 -278
  72. package/v3/dist/internal/utils.d.ts +0 -101
  73. package/v3/dist/internal/utils.js +0 -75
  74. package/v3/dist/machine.d.ts +0 -398
  75. package/v3/dist/machine.js +0 -487
  76. package/v3/dist/schema.d.ts +0 -174
  77. package/v3/dist/schema.js +0 -206
  78. package/v3/dist/slot.d.ts +0 -158
  79. package/v3/dist/slot.js +0 -165
  80. package/v3/dist/supervision.d.ts +0 -97
  81. package/v3/dist/supervision.js +0 -42
  82. package/v3/dist/testing.d.ts +0 -151
  83. package/v3/dist/testing.js +0 -189
  84. /package/{v3/dist/internal/brands.js → dist/internal/machine-definition.js} +0 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cristian Velasquez Ramos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -17,17 +17,16 @@ Use it when a feature has:
17
17
  bun add effect-machine effect
18
18
  ```
19
19
 
20
- `effect` is a peer dependency. The repository validates both the v4 entrypoint
21
- and the `effect-machine/v3` mirror with `@effect/tsgo`, the latest Effect beta,
22
- type-aware oxlint, and Bun tests.
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.
23
22
 
24
23
  ## Core Pattern
25
24
 
26
25
  States and events are schemas. Types, validation, and serialization from one place.
27
26
 
28
27
  ```ts
29
- import { Schema } from "effect";
30
- import { Event, Machine, Slot, State } from "effect-machine";
28
+ import { Cause, Context, Effect, Schema } from "effect";
29
+ import { Event, Machine, State } from "effect-machine";
31
30
 
32
31
  const CheckoutState = State({
33
32
  ReviewingCart: { cartId: Schema.String, totalCents: Schema.Number },
@@ -43,30 +42,43 @@ const CheckoutEvent = Event({
43
42
  Cancel: {},
44
43
  });
45
44
 
46
- const CheckoutSlots = Slot.define({
47
- chargeCard: Slot.fn({ cartId: Schema.String, totalCents: Schema.Number }),
48
- });
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") {}
49
54
 
50
55
  const checkoutMachine = Machine.make({
51
56
  state: CheckoutState,
52
57
  event: CheckoutEvent,
53
- slots: CheckoutSlots,
54
58
  initial: CheckoutState.ReviewingCart({ cartId: "cart_123", totalCents: 4200 }),
55
59
  })
56
60
  .on(CheckoutState.ReviewingCart, CheckoutEvent.Submit, ({ state }) =>
57
- CheckoutState.ChargingCard.derive(state),
61
+ CheckoutState.ChargingCard.with(state),
58
62
  )
59
63
  .on(CheckoutState.ChargingCard, CheckoutEvent.Charged, ({ state, event }) =>
60
- CheckoutState.Confirmed.derive(state, { receiptId: event.receiptId }),
64
+ CheckoutState.Confirmed.with(state, { receiptId: event.receiptId }),
61
65
  )
62
66
  .on(CheckoutState.ChargingCard, CheckoutEvent.Declined, ({ state, event }) =>
63
- CheckoutState.Failed.derive(state, { reason: event.reason }),
67
+ CheckoutState.Failed.with(state, { reason: event.reason }),
64
68
  )
65
69
  .onAny(CheckoutEvent.Cancel, ({ state }) =>
66
- CheckoutState.Failed.derive(state, { reason: "cancelled" }),
70
+ CheckoutState.Failed.with(state, { reason: "cancelled" }),
67
71
  )
68
- .spawn(CheckoutState.ChargingCard, ({ slots, state }) =>
69
- slots.chargeCard({ cartId: state.cartId, totalCents: state.totalCents }),
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
+ },
70
82
  )
71
83
  .final(CheckoutState.Confirmed)
72
84
  .final(CheckoutState.Failed);
@@ -75,42 +87,30 @@ const checkoutMachine = Machine.make({
75
87
  A few things to notice:
76
88
 
77
89
  - Empty variants are values: `State.Idle`. Non-empty are constructors: `State.Loading({ url })`.
78
- - `State.derive(source, overrides)` carries overlapping fields forward without manual copying.
90
+ - `State.with(source, overrides)` carries overlapping fields forward without manual copying.
79
91
  - `.onAny(...)` is a fallback; a specific `.on(...)` wins.
80
92
  - `.spawn(...)` runs work on state entry and cancels it on state exit.
81
93
 
82
94
  The builder also supports `.timeout(state, { duration, event })`, `.postpone(state, event)` for buffering, and `.reenter(...)` for re-running lifecycle on same-state transitions.
83
95
 
84
- ## Slots
96
+ ## Effect Services
85
97
 
86
- Slots separate what a machine needs from how the app provides it. Declare them on the machine, provide implementations where you run it.
98
+ Task, spawn, and background handlers can use standard Effect services. The machine type records each service requirement.
87
99
 
88
100
  ```ts
89
101
  const actor =
90
102
  yield *
91
- Machine.spawn(checkoutMachine, {
92
- slots: {
93
- chargeCard: ({ cartId, totalCents }) =>
94
- Effect.gen(function* () {
95
- const ctx = yield* checkoutMachine.Context;
96
- const result = yield* PaymentService.charge(cartId, totalCents);
97
- yield* ctx.self.send(
98
- result.ok
99
- ? CheckoutEvent.Charged({ receiptId: result.receiptId })
100
- : CheckoutEvent.Declined({ reason: result.error }),
101
- );
102
- }),
103
- },
104
- });
103
+ Machine.spawn(checkoutMachine).pipe(
104
+ Effect.provideService(PaymentService, {
105
+ chargeCard: (cartId) => Effect.succeed({ receiptId: `rcpt_${cartId}` }),
106
+ }),
107
+ );
105
108
  yield * actor.start;
106
109
  ```
107
110
 
108
- The same machine can run with different slot implementations in tests, local apps, or production. Slots are accepted everywhere the machine runs:
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.
109
112
 
110
- - `Machine.spawn(machine, { slots })`
111
- - `Machine.replay(machine, events, { slots })`
112
- - `simulate(machine, events, { slots })`
113
- - `createTestHarness(machine, { slots })`
113
+ Transition handlers in `.on()` and `.reenter()` stay pure. Use services only in `.task()`, `.spawn()`, and `.background()`.
114
114
 
115
115
  ## Running Actors
116
116
 
@@ -118,23 +118,20 @@ The same machine can run with different slot implementations in tests, local app
118
118
 
119
119
  ```ts
120
120
  const program = Effect.gen(function* () {
121
- const actor = yield* Machine.spawn(checkoutMachine, {
122
- slots: {
123
- chargeCard: ({ cartId }) =>
124
- checkoutMachine.Context.pipe(
125
- Effect.flatMap((ctx) =>
126
- ctx.self.send(CheckoutEvent.Charged({ receiptId: `rcpt_${cartId}` })),
127
- ),
128
- ),
129
- },
130
- });
121
+ const actor = yield* Machine.spawn(checkoutMachine);
131
122
  yield* actor.start;
132
123
 
133
124
  yield* actor.send(CheckoutEvent.Submit);
134
125
  const finalState = yield* actor.awaitFinal;
135
126
  });
136
127
 
137
- Effect.runPromise(Effect.scoped(program));
128
+ Effect.runPromise(
129
+ Effect.scoped(program).pipe(
130
+ Effect.provideService(PaymentService, {
131
+ chargeCard: (cartId) => Effect.succeed({ receiptId: `rcpt_${cartId}` }),
132
+ }),
133
+ ),
134
+ );
138
135
  ```
139
136
 
140
137
  Key actor operations:
@@ -145,7 +142,7 @@ Key actor operations:
145
142
  - `ask(event)` returns a typed domain reply (requires `Event.reply(...)`)
146
143
  - `waitFor(...)` / `awaitFinal` for coordination
147
144
  - `stop` interrupts now; `drain` processes the remaining queue first
148
- - `watch(other)` completes when another actor stops
145
+ - `awaitExit` completes when the actor stops
149
146
 
150
147
  For named actors or shared lookup, use an actor system. `system.spawn` auto-starts — no `actor.start` needed:
151
148
 
@@ -182,11 +179,10 @@ import { simulate } from "effect-machine";
182
179
 
183
180
  const result =
184
181
  yield *
185
- simulate(
186
- checkoutMachine,
187
- [CheckoutEvent.Submit, CheckoutEvent.Charged({ receiptId: "rcpt_123" })],
188
- { slots: { chargeCard: () => Effect.void } },
189
- );
182
+ simulate(checkoutMachine, [
183
+ CheckoutEvent.Submit,
184
+ CheckoutEvent.Charged({ receiptId: "rcpt_123" }),
185
+ ]);
190
186
 
191
187
  expect(result.states.map((s) => s._tag)).toEqual(["ReviewingCart", "ChargingCard", "Confirmed"]);
192
188
  ```
package/dist/actor.d.ts CHANGED
@@ -1,15 +1,10 @@
1
1
  import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
3
- import { ProvideSlots, SlotsDef } from "./slot.js";
4
3
  import { ActorExit, Supervision } from "./supervision.js";
5
- import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
6
4
  import { Lifecycle, Machine } from "./machine.js";
7
- import { RuntimeQueuedEvent } from "./internal/runtime.js";
8
- import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
9
-
5
+ import { ProcessEventResult } from "./internal/transition.js";
6
+ import { Context, Effect, Layer, Option, Scope, Stream, SubscriptionRef } from "effect";
10
7
  //#region src/actor.d.ts
11
- /** Discriminated mailbox request — alias for RuntimeQueuedEvent */
12
- type QueuedEvent<E> = RuntimeQueuedEvent<E>;
13
8
  /**
14
9
  * Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
15
10
  */
@@ -37,8 +32,6 @@ interface ActorRef<State extends {
37
32
  readonly id: string;
38
33
  /** Send an event (fire-and-forget). */
39
34
  readonly send: (event: Event) => Effect.Effect<void>;
40
- /** Fire-and-forget alias for send (OTP gen_server:cast). */
41
- readonly cast: (event: Event) => Effect.Effect<void>;
42
35
  /**
43
36
  * Serialized request-reply (OTP gen_server:call).
44
37
  * Event is processed through the queue; caller gets ProcessEventResult back.
@@ -103,15 +96,6 @@ interface ActorRef<State extends {
103
96
  * Set exactly once when the actor terminates (final, stop, drain, or defect).
104
97
  */
105
98
  readonly awaitExit: Effect.Effect<ActorExit<State>>;
106
- /**
107
- * Watch another actor. Returns an Effect that resolves with the exit reason
108
- * when the watched actor terminally stops. Ignores restarts (Step 3).
109
- * Built on the other actor's exitDeferred — authoritative, not system events.
110
- */
111
- readonly watch: (other: {
112
- readonly id: string;
113
- readonly awaitExit: Effect.Effect<ActorExit<unknown>>;
114
- }) => Effect.Effect<ActorExit<unknown>>;
115
99
  /**
116
100
  * Drain: process all remaining events in the queue, then stop.
117
101
  * Unlike `stop` (which interrupts immediately), `drain` lets the actor finish its work.
@@ -167,9 +151,8 @@ interface ActorSystemService {
167
151
  readonly _tag: string;
168
152
  }, E extends {
169
153
  readonly _tag: string;
170
- }, R, SD extends SlotsDef = Record<string, never>>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
154
+ }, R>(id: string, machine: Machine<S, E, R, any, any>, options?: {
171
155
  readonly supervision?: Supervision.Policy;
172
- readonly slots?: ProvideSlots<SD, any>;
173
156
  readonly lifecycle?: Lifecycle<S, E>;
174
157
  }) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
175
158
  /**
@@ -212,20 +195,6 @@ declare const ActorScope_base: Context.ServiceClass<ActorScope, "effect-machine/
212
195
  * Provide via `Machine.scoped` or `Effect.provideService(ActorScope, scope)`.
213
196
  */
214
197
  declare class ActorScope extends ActorScope_base {}
215
- /** Listener set for sync subscriptions */
216
- type Listeners<S> = Set<(state: S) => void>;
217
- /**
218
- * Notify all listeners of state change.
219
- */
220
- declare const notifyListeners: <S>(listeners: Listeners<S>, state: S) => void;
221
- /**
222
- * Build core ActorRef methods.
223
- */
224
- declare const buildActorRefCore: <S extends {
225
- readonly _tag: string;
226
- }, E extends {
227
- readonly _tag: string;
228
- }, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, start: Effect.Effect<void>, system: ActorSystemService, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>>) => ActorRef<S, E>;
229
198
  /**
230
199
  * Create and start an actor for a machine.
231
200
  * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
@@ -234,22 +203,16 @@ declare const createActor: <S extends {
234
203
  readonly _tag: string;
235
204
  }, E extends {
236
205
  readonly _tag: string;
237
- }, R, SD extends SlotsDef>(id: string, machine: Machine<S, E, R, any, any, SD>, options?: {
206
+ }, R>(id: string, machine: Machine<S, E, R, any, any>, options?: {
238
207
  initialState?: S;
239
208
  supervision?: Supervision.Policy;
240
- lifecycle?: Lifecycle<S, E>; /** @internal Called by system after each restart — emits ActorRestarted system event */
209
+ lifecycle?: Lifecycle<S, E>;
210
+ /** @internal Called by system after each restart — emits ActorRestarted system event */
241
211
  onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
242
- } | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
243
- /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
244
- declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
245
- /**
246
- * Create an ActorSystem instance. Must be run in a Scope.
247
- * @internal — use Default layer for normal usage
248
- */
249
- declare const makeSystem: () => Effect.Effect<ActorSystemService, never, Scope.Scope>;
212
+ } | undefined) => Effect.Effect<ActorRef<S, E>, never, R>;
250
213
  /**
251
214
  * Default ActorSystem layer
252
215
  */
253
216
  declare const Default: Layer.Layer<ActorSystem, never, never>;
254
217
  //#endregion
255
- export { ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
218
+ export { ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, type ProcessEventResult, SystemEvent, SystemEventListener, TransitionInfo, createActor };