effect-machine 0.3.2 → 0.6.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
@@ -183,6 +183,53 @@ machine.task(State.Loading, ({ effects, state }) => effects.fetchData({ url: sta
183
183
  });
184
184
  ```
185
185
 
186
+ ### Child Actors
187
+
188
+ Spawn children from `.spawn()` handlers with `self.spawn`. Children are state-scoped — auto-stopped on state exit:
189
+
190
+ ```ts
191
+ machine
192
+ .spawn(State.Active, ({ self }) =>
193
+ Effect.gen(function* () {
194
+ const child = yield* self.spawn("worker-1", workerMachine).pipe(Effect.orDie);
195
+ yield* child.send(WorkerEvent.Start);
196
+ // child auto-stopped when parent exits Active state
197
+ }),
198
+ )
199
+ .build();
200
+
201
+ // Access children externally via actor.system
202
+ const parent = yield * Machine.spawn(parentMachine);
203
+ yield * parent.send(Event.Activate);
204
+ const child = yield * parent.system.get("worker-1"); // Option<ActorRef>
205
+ ```
206
+
207
+ Every actor always has a system — `Machine.spawn` creates an implicit one if no `ActorSystem` is in context.
208
+
209
+ ### System Observation
210
+
211
+ React to actors joining and leaving the system:
212
+
213
+ ```ts
214
+ const system = yield * ActorSystemService;
215
+
216
+ // Sync callback — like ActorRef.subscribe
217
+ const unsub = system.subscribe((event) => {
218
+ // event._tag: "ActorSpawned" | "ActorStopped"
219
+ console.log(`${event._tag}: ${event.id}`);
220
+ });
221
+
222
+ // Sync snapshot of all registered actors
223
+ const actors = system.actors; // ReadonlyMap<string, ActorRef>
224
+
225
+ // Async stream (each subscriber gets own queue)
226
+ yield *
227
+ system.events.pipe(
228
+ Stream.tap((e) => Effect.log(e._tag, e.id)),
229
+ Stream.runDrain,
230
+ );
231
+ ```
232
+
186
233
  ### Testing
187
234
 
188
235
  Test transitions without actors:
@@ -273,6 +320,19 @@ See the [primer](./primer/) for comprehensive documentation:
273
320
  | `actor.awaitFinal` | Wait final state |
274
321
  | `actor.sendAndWait(ev, State.X)` | Send + wait for state |
275
322
  | `actor.subscribe(fn)` | Sync callback |
323
+ | `actor.system` | Access the actor's `ActorSystem` |
324
+ | `actor.children` | Child actors (`ReadonlyMap`) |
325
+
326
+ ### ActorSystem
327
+
328
+ | Method / Property | Description |
329
+ | ---------------------- | ------------------------------------------- |
330
+ | `system.spawn(id, m)` | Spawn actor |
331
+ | `system.get(id)` | Get actor by ID |
332
+ | `system.stop(id)` | Stop actor by ID |
333
+ | `system.actors` | Sync snapshot of all actors (`ReadonlyMap`) |
334
+ | `system.subscribe(fn)` | Sync callback for spawn/stop events |
335
+ | `system.events` | Async `Stream<SystemEvent>` for spawn/stop |
276
336
 
277
337
  ## License
278
338
 
package/dist/actor.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
2
  import { PersistentMachine } from "./persistence/persistent-machine.js";
3
+ import { DuplicateActorError } from "./errors.js";
3
4
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
4
5
  import { PersistentActorRef } from "./persistence/persistent-actor.js";
5
- import { DuplicateActorError } from "./errors.js";
6
6
  import { ActorMetadata, PersistenceAdapterTag, PersistenceError, RestoreResult, VersionConflictError } from "./persistence/adapter.js";
7
7
  import { BuiltMachine, Machine, MachineRef } from "./machine.js";
8
8
  import { Context, Effect, Layer, Option, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
@@ -101,11 +101,37 @@ interface ActorRef<State extends {
101
101
  * Returns unsubscribe function
102
102
  */
103
103
  readonly subscribe: (fn: (state: State) => void) => () => void;
104
+ /**
105
+ * The actor system this actor belongs to.
106
+ * Every actor always has a system — either inherited from context or implicitly created.
107
+ */
108
+ readonly system: ActorSystem;
109
+ /**
110
+ * Child actors spawned via `self.spawn` in this actor's handlers.
111
+ * State-scoped children are auto-removed on state exit.
112
+ */
113
+ readonly children: ReadonlyMap<string, ActorRef<AnyState, unknown>>;
104
114
  }
105
115
  /** Base type for stored actors (internal) */
106
116
  type AnyState = {
107
117
  readonly _tag: string;
108
118
  };
119
+ /**
120
+ * Events emitted by the ActorSystem when actors are spawned or stopped.
121
+ */
122
+ type SystemEvent = {
123
+ readonly _tag: "ActorSpawned";
124
+ readonly id: string;
125
+ readonly actor: ActorRef<AnyState, unknown>;
126
+ } | {
127
+ readonly _tag: "ActorStopped";
128
+ readonly id: string;
129
+ readonly actor: ActorRef<AnyState, unknown>;
130
+ };
131
+ /**
132
+ * Listener callback for system events.
133
+ */
134
+ type SystemEventListener = (event: SystemEvent) => void;
109
135
  /**
110
136
  * Actor system for managing actor lifecycles
111
137
  */
@@ -169,6 +195,21 @@ interface ActorSystem {
169
195
  * Stop an actor by ID
170
196
  */
171
197
  readonly stop: (id: string) => Effect.Effect<boolean>;
198
+ /**
199
+ * Async stream of system events (actor spawned/stopped).
200
+ * Each subscriber gets their own queue — late subscribers miss prior events.
201
+ */
202
+ readonly events: Stream.Stream<SystemEvent>;
203
+ /**
204
+ * Sync snapshot of all currently registered actors.
205
+ * Returns a new Map on each access (not live).
206
+ */
207
+ readonly actors: ReadonlyMap<string, ActorRef<AnyState, unknown>>;
208
+ /**
209
+ * Subscribe to system events synchronously.
210
+ * Returns an unsubscribe function.
211
+ */
212
+ readonly subscribe: (fn: SystemEventListener) => () => void;
172
213
  /**
173
214
  * List all persisted actor metadata.
174
215
  * Returns empty array if adapter doesn't support registry.
@@ -234,7 +275,7 @@ declare const buildActorRefCore: <S extends {
234
275
  readonly _tag: string;
235
276
  }, E extends {
236
277
  readonly _tag: string;
237
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<E>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>) => ActorRef<S, E>;
278
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<E>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>) => ActorRef<S, E>;
238
279
  /**
239
280
  * Create and start an actor for a machine
240
281
  */
@@ -248,4 +289,4 @@ declare const createActor: <S extends {
248
289
  */
249
290
  declare const Default: Layer.Layer<ActorSystem, never, never>;
250
291
  //#endregion
251
- export { ActorRef, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
292
+ export { ActorRef, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, SystemEvent, SystemEventListener, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
package/dist/actor.js CHANGED
@@ -6,10 +6,18 @@ import { processEventCore, resolveTransition, runSpawnEffects } from "./internal
6
6
  import { emitWithTimestamp } from "./internal/inspection.js";
7
7
  import { PersistenceAdapterTag, PersistenceError } from "./persistence/adapter.js";
8
8
  import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
9
- import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, Queue, Ref, Runtime, Scope, SubscriptionRef } from "effect";
9
+ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
10
10
 
11
11
  //#region src/actor.ts
12
12
  /**
13
+ * Actor system: spawning, lifecycle, and event processing.
14
+ *
15
+ * Combines:
16
+ * - ActorRef interface (running actor handle)
17
+ * - ActorSystem service (spawn/stop/get actors)
18
+ * - Actor creation and event loop
19
+ */
20
+ /**
13
21
  * ActorSystem service tag
14
22
  */
15
23
  const ActorSystem = Context.GenericTag("@effect/machine/ActorSystem");
@@ -24,7 +32,7 @@ const notifyListeners = (listeners, state) => {
24
32
  /**
25
33
  * Build core ActorRef methods shared between regular and persistent actors.
26
34
  */
27
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop) => {
35
+ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
28
36
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
29
37
  if (yield* Ref.get(stoppedRef)) return;
30
38
  yield* Queue.offer(eventQueue, event);
@@ -88,7 +96,9 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
88
96
  return () => {
89
97
  listeners.delete(fn);
90
98
  };
91
- }
99
+ },
100
+ system,
101
+ children: childrenMap
92
102
  };
93
103
  };
94
104
  /**
@@ -96,13 +106,34 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
96
106
  */
97
107
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
98
108
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
109
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
110
+ let system;
111
+ let implicitSystemScope;
112
+ if (Option.isSome(existingSystem)) system = existingSystem.value;
113
+ else {
114
+ const scope = yield* Scope.make();
115
+ system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
116
+ implicitSystemScope = scope;
117
+ }
99
118
  const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
100
119
  const eventQueue = yield* Queue.unbounded();
101
120
  const stoppedRef = yield* Ref.make(false);
102
- const self = { send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
103
- if (yield* Ref.get(stoppedRef)) return;
104
- yield* Queue.offer(eventQueue, event);
105
- }) };
121
+ const childrenMap = /* @__PURE__ */ new Map();
122
+ const self = {
123
+ send: Effect.fn("effect-machine.actor.self.send")(function* (event) {
124
+ if (yield* Ref.get(stoppedRef)) return;
125
+ yield* Queue.offer(eventQueue, event);
126
+ }),
127
+ spawn: (childId, childMachine) => Effect.gen(function* () {
128
+ const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
129
+ childrenMap.set(childId, child);
130
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
131
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
132
+ childrenMap.delete(childId);
133
+ }));
134
+ return child;
135
+ })
136
+ };
106
137
  yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", machine.initial._tag);
107
138
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
108
139
  type: "@machine.spawn",
@@ -117,7 +148,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
117
148
  const ctx = {
118
149
  state: machine.initial,
119
150
  event: initEvent,
120
- self
151
+ self,
152
+ system
121
153
  };
122
154
  const { effects: effectSlots } = machine._slots;
123
155
  for (const bg of machine.backgroundEffects) {
@@ -125,12 +157,13 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
125
157
  state: machine.initial,
126
158
  event: initEvent,
127
159
  self,
128
- effects: effectSlots
160
+ effects: effectSlots,
161
+ system
129
162
  }).pipe(Effect.provideService(machine.Context, ctx)));
130
163
  backgroundFibers.push(fiber);
131
164
  }
132
165
  const stateScopeRef = { current: yield* Scope.make() };
133
- yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue);
166
+ yield* runSpawnEffectsWithInspection(machine, machine.initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
134
167
  if (machine.finalStates.has(machine.initial._tag)) {
135
168
  yield* Scope.close(stateScopeRef.current, Exit.void);
136
169
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
@@ -141,9 +174,10 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
141
174
  timestamp
142
175
  }));
143
176
  yield* Ref.set(stoppedRef, true);
144
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid));
177
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
178
+ return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
145
179
  }
146
- const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue));
180
+ const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system));
147
181
  return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
148
182
  const finalState = yield* SubscriptionRef.get(stateRef);
149
183
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
@@ -156,12 +190,13 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
156
190
  yield* Fiber.interrupt(loopFiber);
157
191
  yield* Scope.close(stateScopeRef.current, Exit.void);
158
192
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
159
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid));
193
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
194
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
160
195
  });
161
196
  /**
162
197
  * Main event loop for the actor
163
198
  */
164
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector) {
199
+ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
165
200
  while (true) {
166
201
  const event = yield* Queue.take(eventQueue);
167
202
  const currentState = yield* SubscriptionRef.get(stateRef);
@@ -169,7 +204,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
169
204
  "effect_machine.actor.id": actorId,
170
205
  "effect_machine.state.current": currentState._tag,
171
206
  "effect_machine.event.type": event._tag
172
- } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector))) {
207
+ } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))) {
173
208
  yield* Ref.set(stoppedRef, true);
174
209
  yield* Scope.close(stateScopeRef.current, Exit.void);
175
210
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
@@ -181,7 +216,7 @@ const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine
181
216
  * Process a single event, returning true if the actor should stop.
182
217
  * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
183
218
  */
184
- const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector) {
219
+ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
185
220
  yield* emitWithTimestamp(inspector, (timestamp) => ({
186
221
  type: "@machine.event",
187
222
  actorId,
@@ -189,7 +224,7 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
189
224
  event,
190
225
  timestamp
191
226
  }));
192
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, inspector === void 0 ? void 0 : {
227
+ const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, inspector === void 0 ? void 0 : {
193
228
  onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
194
229
  type: "@machine.effect",
195
230
  actorId,
@@ -242,7 +277,7 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
242
277
  * Wraps the core runSpawnEffects with inspection events and spans.
243
278
  * @internal
244
279
  */
245
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector) {
280
+ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
246
281
  yield* emitWithTimestamp(inspector, (timestamp) => ({
247
282
  type: "@machine.effect",
248
283
  actorId,
@@ -250,7 +285,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffec
250
285
  state,
251
286
  timestamp
252
287
  }));
253
- yield* runSpawnEffects(machine, state, event, self, stateScope, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
288
+ yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
254
289
  type: "@machine.error",
255
290
  actorId,
256
291
  phase: info.phase,
@@ -260,39 +295,58 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffec
260
295
  timestamp
261
296
  })));
262
297
  });
263
- /**
264
- * Internal implementation
265
- */
298
+ /** Notify all system event listeners (sync). */
299
+ const notifySystemListeners = (listeners, event) => {
300
+ for (const listener of listeners) try {
301
+ listener(event);
302
+ } catch {}
303
+ };
266
304
  const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
267
- const actors = MutableHashMap.empty();
305
+ const actorsMap = MutableHashMap.empty();
268
306
  const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
307
+ const eventPubSub = yield* PubSub.unbounded();
308
+ const eventListeners = /* @__PURE__ */ new Set();
309
+ const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
269
310
  yield* Effect.addFinalizer(() => {
270
311
  const stops = [];
271
- MutableHashMap.forEach(actors, (actor) => {
312
+ MutableHashMap.forEach(actorsMap, (actor) => {
272
313
  stops.push(actor.stop);
273
314
  });
274
- return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.asVoid);
315
+ return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
275
316
  });
276
317
  /** Check for duplicate ID, register actor, attach scope cleanup if available */
277
318
  const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
278
- if (MutableHashMap.has(actors, id)) {
319
+ if (MutableHashMap.has(actorsMap, id)) {
279
320
  yield* actor.stop;
280
321
  return yield* new DuplicateActorError({ actorId: id });
281
322
  }
282
- MutableHashMap.set(actors, id, actor);
323
+ const actorRef = actor;
324
+ MutableHashMap.set(actorsMap, id, actorRef);
325
+ yield* emitSystemEvent({
326
+ _tag: "ActorSpawned",
327
+ id,
328
+ actor: actorRef
329
+ });
283
330
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
284
331
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.gen(function* () {
332
+ if (MutableHashMap.has(actorsMap, id)) {
333
+ yield* emitSystemEvent({
334
+ _tag: "ActorStopped",
335
+ id,
336
+ actor: actorRef
337
+ });
338
+ MutableHashMap.remove(actorsMap, id);
339
+ }
285
340
  yield* actor.stop;
286
- MutableHashMap.remove(actors, id);
287
341
  }));
288
342
  return actor;
289
343
  });
290
344
  const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
291
- if (MutableHashMap.has(actors, id)) return yield* new DuplicateActorError({ actorId: id });
345
+ if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
292
346
  return yield* registerActor(id, yield* createActor(id, built._inner));
293
347
  });
294
348
  const spawnPersistent = Effect.fn("effect-machine.actorSystem.spawnPersistent")(function* (id, persistentMachine) {
295
- if (MutableHashMap.has(actors, id)) return yield* new DuplicateActorError({ actorId: id });
349
+ if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
296
350
  const adapter = yield* PersistenceAdapterTag;
297
351
  const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
298
352
  return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
@@ -311,13 +365,19 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
311
365
  });
312
366
  const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
313
367
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
314
- return yield* Effect.sync(() => MutableHashMap.get(actors, id));
368
+ return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
315
369
  });
316
370
  const stop = Effect.fn("effect-machine.actorSystem.stop")(function* (id) {
317
- const maybeActor = MutableHashMap.get(actors, id);
371
+ const maybeActor = MutableHashMap.get(actorsMap, id);
318
372
  if (Option.isNone(maybeActor)) return false;
319
- yield* maybeActor.value.stop;
320
- MutableHashMap.remove(actors, id);
373
+ const actor = maybeActor.value;
374
+ MutableHashMap.remove(actorsMap, id);
375
+ yield* emitSystemEvent({
376
+ _tag: "ActorStopped",
377
+ id,
378
+ actor
379
+ });
380
+ yield* actor.stop;
321
381
  return true;
322
382
  });
323
383
  const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
@@ -329,7 +389,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
329
389
  const restored = [];
330
390
  const failed = [];
331
391
  for (const id of ids) {
332
- if (MutableHashMap.has(actors, id)) continue;
392
+ if (MutableHashMap.has(actorsMap, id)) continue;
333
393
  const result = yield* Effect.either(restore(id, persistentMachine));
334
394
  if (result._tag === "Left") failed.push({
335
395
  id,
@@ -371,6 +431,20 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
371
431
  restore,
372
432
  get,
373
433
  stop,
434
+ events: Stream.fromPubSub(eventPubSub),
435
+ get actors() {
436
+ const snapshot = /* @__PURE__ */ new Map();
437
+ MutableHashMap.forEach(actorsMap, (actor, id) => {
438
+ snapshot.set(id, actor);
439
+ });
440
+ return snapshot;
441
+ },
442
+ subscribe: (fn) => {
443
+ eventListeners.add(fn);
444
+ return () => {
445
+ eventListeners.delete(fn);
446
+ };
447
+ },
374
448
  listPersisted,
375
449
  restoreMany,
376
450
  restoreAll
@@ -1,6 +1,6 @@
1
1
  import { processEventCore, runSpawnEffects } from "../internal/transition.js";
2
- import "../actor.js";
3
- import { Effect, Queue, Ref, Scope } from "effect";
2
+ import { ActorSystem } from "../actor.js";
3
+ import { Effect, Option, Queue, Ref, Scope } from "effect";
4
4
  import { Entity } from "@effect/cluster";
5
5
 
6
6
  //#region src/cluster/entity-machine.ts
@@ -13,8 +13,8 @@ import { Entity } from "@effect/cluster";
13
13
  * Process a single event through the machine using shared core.
14
14
  * Returns the new state after processing.
15
15
  */
16
- const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, hooks) {
17
- const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, hooks);
16
+ const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, system, hooks) {
17
+ const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, hooks);
18
18
  if (result.transitioned) yield* Ref.set(stateRef, result.newState);
19
19
  return result.newState;
20
20
  });
@@ -51,19 +51,25 @@ const EntityMachine = { layer: (entity, machine, options) => {
51
51
  const layer = Effect.fn("effect-machine.cluster.layer")(function* () {
52
52
  const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(Effect.map((opt) => opt._tag === "Some" ? opt.value.entityId : ""));
53
53
  const initialState = options?.initializeState !== void 0 ? options.initializeState(entityId) : machine.initial;
54
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
55
+ if (Option.isNone(existingSystem)) return yield* Effect.die("EntityMachine requires ActorSystem in context");
56
+ const system = existingSystem.value;
54
57
  const internalQueue = yield* Queue.unbounded();
55
- const self = { send: Effect.fn("effect-machine.cluster.self.send")(function* (event) {
56
- yield* Queue.offer(internalQueue, event);
57
- }) };
58
+ const self = {
59
+ send: Effect.fn("effect-machine.cluster.self.send")(function* (event) {
60
+ yield* Queue.offer(internalQueue, event);
61
+ }),
62
+ spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
63
+ };
58
64
  const stateRef = yield* Ref.make(initialState);
59
65
  const stateScopeRef = { current: yield* Scope.make() };
60
- yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, options?.hooks?.onError);
66
+ yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, system, options?.hooks?.onError);
61
67
  const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
62
- yield* processEvent(machine, stateRef, yield* Queue.take(internalQueue), self, stateScopeRef, options?.hooks);
68
+ yield* processEvent(machine, stateRef, yield* Queue.take(internalQueue), self, stateScopeRef, system, options?.hooks);
63
69
  });
64
70
  yield* Effect.forkScoped(Effect.forever(runInternalEvent()));
65
71
  return entity.of({
66
- Send: (envelope) => processEvent(machine, stateRef, envelope.payload.event, self, stateScopeRef, options?.hooks),
72
+ Send: (envelope) => processEvent(machine, stateRef, envelope.payload.event, self, stateScopeRef, system, options?.hooks),
67
73
  GetState: () => Ref.get(stateRef)
68
74
  });
69
75
  });
package/dist/index.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
3
3
  import { PersistenceConfig, PersistentMachine, isPersistentMachine } from "./persistence/persistent-machine.js";
4
- import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
5
4
  import { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
5
+ import { PersistentActorRef, createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
6
6
  import { ActorMetadata, PersistedEvent, PersistenceAdapter, PersistenceAdapterTag, PersistenceError, RestoreFailure, RestoreResult, Snapshot, VersionConflictError } from "./persistence/adapter.js";
7
7
  import { InMemoryPersistenceAdapter, makeInMemoryPersistenceAdapter } from "./persistence/adapters/in-memory.js";
8
8
  import "./persistence/index.js";
9
9
  import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, ProvideHandlers, SpawnEffect, StateHandlerContext, Transition, machine_d_exports } from "./machine.js";
10
- import { ActorRef, ActorSystem, Default } from "./actor.js";
10
+ import { ActorRef, ActorSystem, Default, SystemEvent, SystemEventListener } from "./actor.js";
11
11
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
12
12
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, SpawnEvent, StopEvent, TransitionEvent, collectingInspector, consoleInspector, makeInspector } from "./inspection.js";
13
- export { type ActorMetadata, type ActorRef, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, InMemoryPersistenceAdapter, type InspectionEvent, type Inspector, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type TestHarness, type TestHarnessOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, restorePersistentActor, simulate };
13
+ export { type ActorMetadata, type ActorRef, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, InMemoryPersistenceAdapter, type InspectionEvent, type Inspector, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, type PersistOptions, type PersistedEvent, type PersistenceAdapter, PersistenceAdapterTag, type PersistenceConfig, PersistenceError, type PersistentActorRef, type PersistentMachine, type ProvideHandlers, ProvisionValidationError, type RestoreFailure, type RestoreResult, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type Snapshot, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TestHarness, type TestHarnessOptions, type Transition, type TransitionEvent, UnprovidedSlotsError, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, consoleInspector, createPersistentActor, createTestHarness, isPersistentMachine, makeInMemoryPersistenceAdapter, makeInspector, restorePersistentActor, simulate };
@@ -1,5 +1,6 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
2
  import { BuiltMachine, Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
+ import { ActorSystem } from "../actor.js";
3
4
  import { Cause, Effect, Scope } from "effect";
4
5
 
5
6
  //#region src/internal/transition.d.ts
@@ -28,7 +29,7 @@ declare const runTransitionHandler: <S extends {
28
29
  readonly _tag: string;
29
30
  }, E extends {
30
31
  readonly _tag: string;
31
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
32
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
32
33
  /**
33
34
  * Execute a transition for a given state and event.
34
35
  * Handles transition resolution, handler invocation, and guard/effect slot creation.
@@ -44,7 +45,7 @@ declare const executeTransition: <S extends {
44
45
  readonly _tag: string;
45
46
  }, E extends {
46
47
  readonly _tag: string;
47
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>) => Effect.Effect<{
48
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystem) => Effect.Effect<{
48
49
  newState: S;
49
50
  transitioned: boolean;
50
51
  reenter: boolean;
@@ -102,7 +103,7 @@ declare const processEventCore: <S extends {
102
103
  readonly _tag: string;
103
104
  }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
104
105
  current: Scope.CloseableScope;
105
- }, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
106
+ }, system: ActorSystem, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
106
107
  newState: S;
107
108
  previousState: S;
108
109
  transitioned: boolean;
@@ -118,7 +119,7 @@ declare const runSpawnEffects: <S extends {
118
119
  readonly _tag: string;
119
120
  }, E extends {
120
121
  readonly _tag: string;
121
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
122
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
122
123
  /**
123
124
  * Resolve which transition should fire for a given state and event.
124
125
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -23,11 +23,12 @@ import { Cause, Effect, Exit, Scope } from "effect";
23
23
  *
24
24
  * @internal
25
25
  */
26
- const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* (machine, transition, state, event, self) {
26
+ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* (machine, transition, state, event, self, system) {
27
27
  const ctx = {
28
28
  state,
29
29
  event,
30
- self
30
+ self,
31
+ system
31
32
  };
32
33
  const { guards, effects } = machine._slots;
33
34
  const handlerCtx = {
@@ -50,7 +51,7 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
50
51
  *
51
52
  * @internal
52
53
  */
53
- const executeTransition = Effect.fn("effect-machine.executeTransition")(function* (machine, currentState, event, self) {
54
+ const executeTransition = Effect.fn("effect-machine.executeTransition")(function* (machine, currentState, event, self, system) {
54
55
  const transition = resolveTransition(machine, currentState, event);
55
56
  if (transition === void 0) return {
56
57
  newState: currentState,
@@ -58,7 +59,7 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
58
59
  reenter: false
59
60
  };
60
61
  return {
61
- newState: yield* runTransitionHandler(machine, transition, currentState, event, self),
62
+ newState: yield* runTransitionHandler(machine, transition, currentState, event, self, system),
62
63
  transitioned: true,
63
64
  reenter: transition.reenter === true
64
65
  };
@@ -75,8 +76,8 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
75
76
  *
76
77
  * @internal
77
78
  */
78
- const processEventCore = Effect.fn("effect-machine.processEventCore")(function* (machine, currentState, event, self, stateScopeRef, hooks) {
79
- const result = yield* executeTransition(machine, currentState, event, self).pipe(Effect.catchAllCause((cause) => {
79
+ const processEventCore = Effect.fn("effect-machine.processEventCore")(function* (machine, currentState, event, self, stateScopeRef, system, hooks) {
80
+ const result = yield* executeTransition(machine, currentState, event, self, system).pipe(Effect.catchAllCause((cause) => {
80
81
  if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
81
82
  const onError = hooks?.onError;
82
83
  if (onError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
@@ -101,7 +102,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
101
102
  stateScopeRef.current = yield* Scope.make();
102
103
  if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
103
104
  if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
104
- yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, hooks?.onError);
105
+ yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, hooks?.onError);
105
106
  }
106
107
  return {
107
108
  newState,
@@ -116,12 +117,13 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
116
117
  *
117
118
  * @internal
118
119
  */
119
- const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, onError) {
120
+ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, onError) {
120
121
  const spawnEffects = findSpawnEffects(machine, state._tag);
121
122
  const ctx = {
122
123
  state,
123
124
  event,
124
- self
125
+ self,
126
+ system
125
127
  };
126
128
  const { effects: effectSlots } = machine._slots;
127
129
  const reportError = onError;
@@ -130,7 +132,8 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
130
132
  state,
131
133
  event,
132
134
  self,
133
- effects: effectSlots
135
+ effects: effectSlots,
136
+ system
134
137
  }).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
135
138
  if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
136
139
  if (reportError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
@@ -1,3 +1,4 @@
1
+ import { ActorSystem } from "../actor.js";
1
2
  import { Effect } from "effect";
2
3
 
3
4
  //#region src/internal/utils.d.ts
@@ -48,5 +49,12 @@ declare const getTag: (constructorOrValue: {
48
49
  })) => string;
49
50
  /** Check if a value is an Effect */
50
51
  declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unknown, unknown>;
52
+ /**
53
+ * Stub ActorSystem that dies on any method call.
54
+ * Used in contexts where spawning/system access isn't supported
55
+ * (testing simulation, persistent actor replay).
56
+ * @internal
57
+ */
58
+ declare const stubSystem: ActorSystem;
51
59
  //#endregion
52
- export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect };
60
+ export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect, stubSystem };
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Stream } from "effect";
2
2
 
3
3
  //#region src/internal/utils.ts
4
4
  /**
@@ -26,6 +26,26 @@ const getTag = (constructorOrValue) => {
26
26
  };
27
27
  /** Check if a value is an Effect */
28
28
  const isEffect = (value) => typeof value === "object" && value !== null && Effect.EffectTypeId in value;
29
+ /**
30
+ * Stub ActorSystem that dies on any method call.
31
+ * Used in contexts where spawning/system access isn't supported
32
+ * (testing simulation, persistent actor replay).
33
+ * @internal
34
+ */
35
+ const stubSystem = {
36
+ spawn: () => Effect.die("spawn not supported in stub system"),
37
+ restore: () => Effect.die("restore not supported in stub system"),
38
+ get: () => Effect.die("get not supported in stub system"),
39
+ stop: () => Effect.die("stop not supported in stub system"),
40
+ events: Stream.empty,
41
+ get actors() {
42
+ return /* @__PURE__ */ new Map();
43
+ },
44
+ subscribe: () => () => {},
45
+ listPersisted: () => Effect.die("listPersisted not supported in stub system"),
46
+ restoreMany: () => Effect.die("restoreMany not supported in stub system"),
47
+ restoreAll: () => Effect.die("restoreAll not supported in stub system")
48
+ };
29
49
 
30
50
  //#endregion
31
- export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect };
51
+ export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
package/dist/machine.d.ts CHANGED
@@ -3,9 +3,10 @@ import { TransitionResult } from "./internal/utils.js";
3
3
  import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
4
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
5
  import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
6
+ import { DuplicateActorError } from "./errors.js";
6
7
  import { findTransitions } from "./internal/transition.js";
7
8
  import "./persistence/index.js";
8
- import { ActorRef } from "./actor.js";
9
+ import { ActorRef, ActorSystem } from "./actor.js";
9
10
  import { Cause, Context, Effect, Schedule, Schema, Scope } from "effect";
10
11
 
11
12
  //#region src/machine.d.ts
@@ -17,6 +18,11 @@ declare namespace machine_d_exports {
17
18
  */
18
19
  interface MachineRef<Event> {
19
20
  readonly send: (event: Event) => Effect.Effect<void>;
21
+ readonly spawn: <S2 extends {
22
+ readonly _tag: string;
23
+ }, E2 extends {
24
+ readonly _tag: string;
25
+ }, R2>(id: string, machine: BuiltMachine<S2, E2, R2>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
20
26
  }
21
27
  /**
22
28
  * Handler context passed to transition handlers
@@ -35,6 +41,7 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
35
41
  readonly event: Event;
36
42
  readonly self: MachineRef<Event>;
37
43
  readonly effects: EffectSlots<ED>;
44
+ readonly system: ActorSystem;
38
45
  }
39
46
  /**
40
47
  * Transition handler function
@@ -1,5 +1,5 @@
1
- import { PersistentActorRef } from "./persistent-actor.js";
2
1
  import { DuplicateActorError } from "../errors.js";
2
+ import { PersistentActorRef } from "./persistent-actor.js";
3
3
  import { Context, Effect, Option, Schema } from "effect";
4
4
 
5
5
  //#region src/persistence/adapter.d.ts
@@ -1,9 +1,9 @@
1
1
  import { Inspector } from "../inspection.js";
2
- import { INTERNAL_INIT_EVENT } from "../internal/utils.js";
2
+ import { INTERNAL_INIT_EVENT, stubSystem } from "../internal/utils.js";
3
3
  import { processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler } from "../internal/transition.js";
4
4
  import { emitWithTimestamp } from "../internal/inspection.js";
5
5
  import { PersistenceAdapterTag } from "./adapter.js";
6
- import { buildActorRefCore, notifyListeners } from "../actor.js";
6
+ import { ActorSystem, buildActorRefCore, notifyListeners } from "../actor.js";
7
7
  import { Cause, Clock, Effect, Exit, Fiber, Option, Queue, Ref, Schedule, Scope, SubscriptionRef } from "effect";
8
8
 
9
9
  //#region src/persistence/persistent-actor.ts
@@ -20,7 +20,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
20
20
  for (const persistedEvent of events) {
21
21
  if (stopVersion !== void 0 && persistedEvent.version > stopVersion) break;
22
22
  const transition = resolveTransition(machine, state, persistedEvent.event);
23
- if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self);
23
+ if (transition !== void 0) state = yield* runTransitionHandler(machine, transition, state, persistedEvent.event, self, stubSystem);
24
24
  version = persistedEvent.version;
25
25
  }
26
26
  return {
@@ -31,7 +31,7 @@ const replayEvents = Effect.fn("effect-machine.persistentActor.replayEvents")(fu
31
31
  /**
32
32
  * Build PersistentActorRef with all methods
33
33
  */
34
- const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter) => {
34
+ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, stop, adapter, system, childrenMap) => {
35
35
  const { machine, persistence } = persistentMachine;
36
36
  const typedMachine = machine;
37
37
  const persist = Effect.gen(function* () {
@@ -45,12 +45,15 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
45
45
  const version = Ref.get(versionRef).pipe(Effect.withSpan("effect-machine.persistentActor.version"));
46
46
  const replayTo = Effect.fn("effect-machine.persistentActor.replayTo")(function* (targetVersion) {
47
47
  if (targetVersion <= (yield* Ref.get(versionRef))) {
48
+ const dummySelf = {
49
+ send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void),
50
+ spawn: () => Effect.die("spawn not supported in replay")
51
+ };
48
52
  const maybeSnapshot = yield* adapter.loadSnapshot(id, persistence.stateSchema);
49
53
  if (Option.isSome(maybeSnapshot)) {
50
54
  const snapshot = maybeSnapshot.value;
51
55
  if (snapshot.version <= targetVersion) {
52
56
  const events = yield* adapter.loadEvents(id, persistence.eventSchema, snapshot.version);
53
- const dummySelf = { send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void) };
54
57
  const result = yield* replayEvents(typedMachine, snapshot.state, events, dummySelf, targetVersion);
55
58
  yield* SubscriptionRef.set(stateRef, result.state);
56
59
  yield* Ref.set(versionRef, result.version);
@@ -59,7 +62,6 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
59
62
  } else {
60
63
  const events = yield* adapter.loadEvents(id, persistence.eventSchema);
61
64
  if (events.length > 0) {
62
- const dummySelf = { send: Effect.fn("effect-machine.persistentActor.replay.send")((_event) => Effect.void) };
63
65
  const result = yield* replayEvents(typedMachine, typedMachine.initial, events, dummySelf, targetVersion);
64
66
  yield* SubscriptionRef.set(stateRef, result.state);
65
67
  yield* Ref.set(versionRef, result.version);
@@ -69,7 +71,7 @@ const buildPersistentActorRef = (id, persistentMachine, stateRef, versionRef, ev
69
71
  }
70
72
  });
71
73
  return {
72
- ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop),
74
+ ...buildActorRefCore(id, typedMachine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap),
73
75
  persist,
74
76
  version,
75
77
  replayTo
@@ -84,13 +86,28 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
84
86
  const adapter = yield* PersistenceAdapterTag;
85
87
  const { machine, persistence } = persistentMachine;
86
88
  const typedMachine = machine;
89
+ const existingSystem = yield* Effect.serviceOption(ActorSystem);
90
+ if (Option.isNone(existingSystem)) return yield* Effect.die("PersistentActor requires ActorSystem in context");
91
+ const system = existingSystem.value;
87
92
  const inspector = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
88
93
  const eventQueue = yield* Queue.unbounded();
89
94
  const stoppedRef = yield* Ref.make(false);
90
- const self = { send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
91
- if (yield* Ref.get(stoppedRef)) return;
92
- yield* Queue.offer(eventQueue, event);
93
- }) };
95
+ const childrenMap = /* @__PURE__ */ new Map();
96
+ const self = {
97
+ send: Effect.fn("effect-machine.persistentActor.self.send")(function* (event) {
98
+ if (yield* Ref.get(stoppedRef)) return;
99
+ yield* Queue.offer(eventQueue, event);
100
+ }),
101
+ spawn: (childId, childMachine) => Effect.gen(function* () {
102
+ const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
103
+ childrenMap.set(childId, child);
104
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
105
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
106
+ childrenMap.delete(childId);
107
+ }));
108
+ return child;
109
+ })
110
+ };
94
111
  let resolvedInitial;
95
112
  let initialVersion;
96
113
  if (Option.isSome(initialSnapshot)) {
@@ -131,7 +148,8 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
131
148
  const initCtx = {
132
149
  state: resolvedInitial,
133
150
  event: initEvent,
134
- self
151
+ self,
152
+ system
135
153
  };
136
154
  const { effects: effectSlots } = typedMachine._slots;
137
155
  for (const bg of typedMachine.backgroundEffects) {
@@ -139,12 +157,13 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
139
157
  state: resolvedInitial,
140
158
  event: initEvent,
141
159
  self,
142
- effects: effectSlots
160
+ effects: effectSlots,
161
+ system
143
162
  }).pipe(Effect.provideService(typedMachine.Context, initCtx)));
144
163
  backgroundFibers.push(fiber);
145
164
  }
146
165
  const stateScopeRef = { current: yield* Scope.make() };
147
- yield* runSpawnEffectsWithInspection(typedMachine, resolvedInitial, initEvent, self, stateScopeRef.current, id, inspector);
166
+ yield* runSpawnEffectsWithInspection(typedMachine, resolvedInitial, initEvent, self, stateScopeRef.current, id, inspector, system);
148
167
  if (typedMachine.finalStates.has(resolvedInitial._tag)) {
149
168
  yield* Scope.close(stateScopeRef.current, Exit.void);
150
169
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
@@ -157,9 +176,9 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
157
176
  finalState: resolvedInitial,
158
177
  timestamp
159
178
  }));
160
- return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter);
179
+ return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
161
180
  }
162
- const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector));
181
+ const loopFiber = yield* Effect.forkDaemon(persistentEventLoop(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system));
163
182
  return buildPersistentActorRef(id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
164
183
  const finalState = yield* SubscriptionRef.get(stateRef);
165
184
  yield* emitWithTimestamp(inspector, (timestamp) => ({
@@ -174,12 +193,12 @@ const createPersistentActor = Effect.fn("effect-machine.persistentActor.spawn")(
174
193
  yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
175
194
  yield* Fiber.interrupt(snapshotFiber);
176
195
  yield* Fiber.interrupt(persistenceFiber);
177
- }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter);
196
+ }).pipe(Effect.withSpan("effect-machine.persistentActor.stop"), Effect.asVoid), adapter, system, childrenMap);
178
197
  });
179
198
  /**
180
199
  * Main event loop for persistent actor
181
200
  */
182
- const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector) {
201
+ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop")(function* (id, persistentMachine, stateRef, versionRef, eventQueue, stoppedRef, self, listeners, adapter, createdAt, stateScopeRef, backgroundFibers, snapshotQueue, snapshotEnabledRef, persistenceQueue, snapshotFiber, persistenceFiber, inspector, system) {
183
202
  const { machine, persistence } = persistentMachine;
184
203
  const typedMachine = machine;
185
204
  const hooks = inspector === void 0 ? void 0 : {
@@ -219,7 +238,7 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
219
238
  event,
220
239
  timestamp
221
240
  }));
222
- const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, hooks);
241
+ const result = yield* processEventCore(typedMachine, currentState, event, self, stateScopeRef, system, hooks);
223
242
  if (!result.transitioned) continue;
224
243
  const newVersion = currentVersion + 1;
225
244
  yield* Ref.set(versionRef, newVersion);
@@ -259,7 +278,7 @@ const persistentEventLoop = Effect.fn("effect-machine.persistentActor.eventLoop"
259
278
  * Run spawn effects with inspection and tracing.
260
279
  * @internal
261
280
  */
262
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector) {
281
+ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
263
282
  yield* emitWithTimestamp(inspector, (timestamp) => ({
264
283
  type: "@machine.effect",
265
284
  actorId,
@@ -267,7 +286,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.persistentActor.
267
286
  state,
268
287
  timestamp
269
288
  }));
270
- yield* runSpawnEffects(machine, state, event, self, stateScope, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
289
+ yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
271
290
  type: "@machine.error",
272
291
  actorId,
273
292
  phase: info.phase,
package/dist/slot.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ActorSystem } from "./actor.js";
1
2
  import { Context, Effect, Schema } from "effect";
2
3
 
3
4
  //#region src/slot.d.ts
@@ -45,6 +46,7 @@ interface MachineContext<State, Event, Self> {
45
46
  readonly state: State;
46
47
  readonly event: Event;
47
48
  readonly self: Self;
49
+ readonly system: ActorSystem;
48
50
  }
49
51
  /**
50
52
  * Shared Context tag for all machines.
package/dist/testing.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { stubSystem } from "./internal/utils.js";
1
2
  import { AssertionError } from "./errors.js";
2
3
  import { BuiltMachine } from "./machine.js";
3
4
  import { executeTransition } from "./internal/transition.js";
@@ -26,11 +27,14 @@ import { Effect, SubscriptionRef } from "effect";
26
27
  */
27
28
  const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
28
29
  const machine = input instanceof BuiltMachine ? input._inner : input;
29
- const dummySelf = { send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void) };
30
+ const dummySelf = {
31
+ send: Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void),
32
+ spawn: () => Effect.die("spawn not supported in simulation")
33
+ };
30
34
  let currentState = machine.initial;
31
35
  const states = [currentState];
32
36
  for (const event of events) {
33
- const result = yield* executeTransition(machine, currentState, event, dummySelf);
37
+ const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
34
38
  if (!result.transitioned) continue;
35
39
  currentState = result.newState;
36
40
  states.push(currentState);
@@ -110,13 +114,16 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
110
114
  */
111
115
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
112
116
  const machine = input instanceof BuiltMachine ? input._inner : input;
113
- const dummySelf = { send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void) };
117
+ const dummySelf = {
118
+ send: Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void),
119
+ spawn: () => Effect.die("spawn not supported in test harness")
120
+ };
114
121
  const stateRef = yield* SubscriptionRef.make(machine.initial);
115
122
  return {
116
123
  state: stateRef,
117
124
  send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
118
125
  const currentState = yield* SubscriptionRef.get(stateRef);
119
- const result = yield* executeTransition(machine, currentState, event, dummySelf);
126
+ const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem);
120
127
  if (!result.transitioned) return currentState;
121
128
  const newState = result.newState;
122
129
  yield* SubscriptionRef.set(stateRef, newState);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.3.2",
3
+ "version": "0.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
@@ -41,21 +41,21 @@
41
41
  "release": "bun run build && changeset publish"
42
42
  },
43
43
  "dependencies": {
44
- "effect": "^3.19.15"
44
+ "effect": "^3.19.16"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@changesets/changelog-github": "^0.5.2",
48
48
  "@changesets/cli": "^2.29.8",
49
- "@effect/cluster": "^0.56.1",
49
+ "@effect/cluster": "^0.56.2",
50
50
  "@effect/experimental": "^0.58.0",
51
- "@effect/language-service": "^0.72.0",
51
+ "@effect/language-service": "^0.73.0",
52
52
  "@effect/rpc": "^0.73.0",
53
- "@types/bun": "latest",
53
+ "@types/bun": "1.3.8",
54
54
  "concurrently": "^9.2.1",
55
55
  "effect-bun-test": "^0.1.0",
56
- "lefthook": "^2.0.15",
57
- "oxfmt": "^0.26.0",
58
- "oxlint": "^1.41.0",
56
+ "lefthook": "^2.1.0",
57
+ "oxfmt": "^0.28.0",
58
+ "oxlint": "^1.43.0",
59
59
  "tsdown": "^0.20.3",
60
60
  "typescript": "^5.9.3"
61
61
  },
@@ -70,5 +70,8 @@
70
70
  "@effect/rpc": {
71
71
  "optional": true
72
72
  }
73
+ },
74
+ "overrides": {
75
+ "effect": "^3.19.16"
73
76
  }
74
77
  }