effect-machine 0.12.0 → 0.13.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/dist/actor.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { Inspector } from "./inspection.js";
2
- import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
3
- import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
2
+ import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
4
3
  import { emitWithTimestamp } from "./internal/inspection.js";
5
- import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
6
- import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schema, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
4
+ import { ActorStoppedError, DuplicateActorError } from "./errors.js";
5
+ import { createRuntime } from "./internal/runtime.js";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
7
7
  //#region src/actor.ts
8
8
  /**
9
9
  * Actor system: spawning, lifecycle, and event processing.
@@ -28,10 +28,11 @@ const notifyListeners = (listeners, state) => {
28
28
  /**
29
29
  * Build core ActorRef methods.
30
30
  */
31
- const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
31
+ const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
32
32
  const send = Effect.fn("effect-machine.actor.send")(function* (event) {
33
33
  if (yield* Ref.get(stoppedRef)) return;
34
- yield* Queue.offer(eventQueue, {
34
+ const q = yield* Ref.get(eventQueueRef);
35
+ yield* Queue.offer(q, {
35
36
  _tag: "send",
36
37
  event
37
38
  });
@@ -49,7 +50,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
49
50
  }
50
51
  const reply = yield* Deferred.make();
51
52
  pendingReplies.add(reply);
52
- yield* Queue.offer(eventQueue, {
53
+ const q = yield* Ref.get(eventQueueRef);
54
+ yield* Queue.offer(q, {
53
55
  _tag: "call",
54
56
  event,
55
57
  reply
@@ -66,7 +68,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
66
68
  if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
67
69
  const reply = yield* Deferred.make();
68
70
  pendingReplies.add(reply);
69
- yield* Queue.offer(eventQueue, {
71
+ const q = yield* Ref.get(eventQueueRef);
72
+ yield* Queue.offer(q, {
70
73
  _tag: "ask",
71
74
  event,
72
75
  reply
@@ -126,12 +129,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
126
129
  listeners.delete(fn);
127
130
  };
128
131
  },
132
+ awaitExit: Deferred.await(exitDeferred),
133
+ watch: (other) => other.awaitExit,
134
+ drain: Effect.gen(function* () {
135
+ if (yield* Ref.get(stoppedRef)) return;
136
+ const q = yield* Ref.get(eventQueueRef);
137
+ const done = yield* Deferred.make();
138
+ yield* Queue.offer(q, {
139
+ _tag: "drain",
140
+ done
141
+ });
142
+ yield* Deferred.await(done);
143
+ }).pipe(Effect.asVoid),
129
144
  sync: {
130
145
  send: (event) => {
131
- if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
132
- _tag: "send",
133
- event
134
- }));
146
+ if (!Effect.runSync(Ref.get(stoppedRef))) {
147
+ const q = Effect.runSync(Ref.get(eventQueueRef));
148
+ Effect.runSync(Queue.offer(q, {
149
+ _tag: "send",
150
+ event
151
+ }));
152
+ }
135
153
  },
136
154
  stop: () => Effect.runFork(stop),
137
155
  snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
@@ -144,12 +162,41 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
144
162
  children: childrenMap
145
163
  };
146
164
  };
165
+ /** Build ProcessEventHooks from an inspector */
166
+ const buildInspectionHooks = (actorId, inspector) => ({
167
+ onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
168
+ type: "@machine.effect",
169
+ actorId,
170
+ effectType: "spawn",
171
+ state,
172
+ timestamp
173
+ })),
174
+ onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
175
+ type: "@machine.transition",
176
+ actorId,
177
+ fromState: from,
178
+ toState: to,
179
+ event: ev,
180
+ timestamp
181
+ })),
182
+ onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
183
+ type: "@machine.error",
184
+ actorId,
185
+ phase: info.phase,
186
+ state: info.state,
187
+ event: info.event,
188
+ error: Cause.pretty(info.cause),
189
+ timestamp
190
+ }))
191
+ });
147
192
  /**
148
- * Create and start an actor for a machine
193
+ * Create and start an actor for a machine.
194
+ * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
149
195
  */
150
196
  const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
151
197
  const initial = options?.initialState ?? machine.initial;
152
198
  yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
199
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
153
200
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
154
201
  let system;
155
202
  let implicitSystemScope;
@@ -160,102 +207,149 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
160
207
  implicitSystemScope = scope;
161
208
  }
162
209
  const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
163
- const eventQueue = yield* Queue.unbounded();
164
- const stoppedRef = yield* Ref.make(false);
165
210
  const childrenMap = /* @__PURE__ */ new Map();
166
- const deferredReplyRef = { current: void 0 };
167
- const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
168
- if (yield* Ref.get(stoppedRef)) return;
169
- yield* Queue.offer(eventQueue, {
170
- _tag: "send",
171
- event
172
- });
173
- });
174
- const self = {
175
- send: selfSend,
176
- cast: selfSend,
177
- spawn: (childId, childMachine) => Effect.gen(function* () {
178
- const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
179
- childrenMap.set(childId, child);
180
- const maybeScope = yield* Effect.serviceOption(Scope.Scope);
181
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
182
- childrenMap.delete(childId);
183
- }));
184
- return child;
185
- }),
186
- reply: (value) => Effect.sync(() => {
187
- const deferred = deferredReplyRef.current;
188
- if (deferred !== void 0) {
189
- deferredReplyRef.current = void 0;
190
- Effect.runFork(Deferred.succeed(deferred, value));
191
- return true;
192
- }
193
- return false;
194
- })
195
- };
196
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
211
+ const pendingReplies = /* @__PURE__ */ new Set();
212
+ const listeners = /* @__PURE__ */ new Set();
213
+ const transitionsPubSub = yield* PubSub.unbounded();
197
214
  yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
198
215
  type: "@machine.spawn",
199
216
  actorId: id,
200
217
  initialState: initial,
201
218
  timestamp
202
219
  }));
220
+ const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
221
+ const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
222
+ value: initial,
223
+ enumerable: true
224
+ } }) : machine;
203
225
  const stateRef = yield* SubscriptionRef.make(initial);
204
- const listeners = /* @__PURE__ */ new Set();
205
- const backgroundFibers = [];
206
- const initEvent = { _tag: INTERNAL_INIT_EVENT };
207
- const ctx = {
208
- actorId: id,
209
- state: initial,
210
- event: initEvent,
211
- self,
212
- system
226
+ const stoppedRef = yield* Ref.make(false);
227
+ const initialQueue = yield* Queue.unbounded();
228
+ const eventQueueRef = yield* Ref.make(initialQueue);
229
+ const terminalExitDeferred = yield* Deferred.make();
230
+ let stopEmitted = false;
231
+ const runtimeRef = { current: void 0 };
232
+ /** Build lifecycle hooks for a generation */
233
+ const buildLifecycle = () => {
234
+ stopEmitted = false;
235
+ return {
236
+ onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
237
+ type: "@machine.event",
238
+ actorId: id,
239
+ state,
240
+ event,
241
+ timestamp
242
+ })) : void 0,
243
+ onStateChange: (result, _event) => Effect.gen(function* () {
244
+ notifyListeners(listeners, result.newState);
245
+ yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
246
+ if (result.lifecycleRan) {
247
+ yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
248
+ yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
249
+ }
250
+ }),
251
+ onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
252
+ fromState: result.previousState,
253
+ toState: result.newState,
254
+ event
255
+ }).pipe(Effect.asVoid) : Effect.void,
256
+ onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
257
+ stopEmitted = true;
258
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
259
+ type: "@machine.stop",
260
+ actorId: id,
261
+ finalState: state,
262
+ timestamp
263
+ }));
264
+ }) : void 0,
265
+ onShutdown: () => Effect.gen(function* () {
266
+ if (!stopEmitted) {
267
+ const finalState = yield* SubscriptionRef.get(stateRef);
268
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
269
+ type: "@machine.stop",
270
+ actorId: id,
271
+ finalState,
272
+ timestamp
273
+ }));
274
+ }
275
+ yield* settlePendingReplies(pendingReplies, id);
276
+ }),
277
+ onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
278
+ type: "@machine.effect",
279
+ actorId: id,
280
+ effectType: "spawn",
281
+ state,
282
+ timestamp
283
+ })) : void 0
284
+ };
213
285
  };
214
- const { effects: effectSlots } = machine._slots;
215
- for (const bg of machine.backgroundEffects) {
216
- const fiber = yield* Effect.forkDetach(bg.handler({
217
- actorId: id,
218
- state: initial,
219
- event: initEvent,
220
- self,
221
- effects: effectSlots,
222
- system
223
- }).pipe(Effect.provideService(machine.Context, ctx)));
224
- backgroundFibers.push(fiber);
225
- }
226
- const stateScopeRef = { current: yield* Scope.make() };
227
- yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
228
- if (machine.finalStates.has(initial._tag)) {
229
- yield* Scope.close(stateScopeRef.current, Exit.void);
230
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
231
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
232
- type: "@machine.stop",
233
- actorId: id,
234
- finalState: initial,
235
- timestamp
236
- }));
237
- yield* Ref.set(stoppedRef, true);
238
- if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
239
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
240
- }
241
- const pendingReplies = /* @__PURE__ */ new Set();
242
- const transitionsPubSub = yield* PubSub.unbounded();
243
- const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub, deferredReplyRef));
244
- return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
245
- const finalState = yield* SubscriptionRef.get(stateRef);
246
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
247
- type: "@machine.stop",
248
- actorId: id,
249
- finalState,
250
- timestamp
251
- }));
252
- yield* Ref.set(stoppedRef, true);
253
- yield* Fiber.interrupt(loopFiber);
254
- yield* settlePendingReplies(pendingReplies, id);
255
- yield* Scope.close(stateScopeRef.current, Exit.void);
256
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
286
+ /** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
287
+ const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
288
+ actorId: id,
289
+ hooks,
290
+ skipFinalizer: true,
291
+ cellResources: {
292
+ stateRef,
293
+ stoppedRef,
294
+ eventQueue: currentQueue
295
+ },
296
+ lifecycle: buildLifecycle(),
297
+ wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
298
+ "effect_machine.actor.id": id,
299
+ "effect_machine.state.current": state._tag,
300
+ "effect_machine.event.type": event._tag
301
+ } })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
302
+ onChildSpawned: (childId, child) => Effect.gen(function* () {
303
+ childrenMap.set(childId, child);
304
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
305
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
306
+ childrenMap.delete(childId);
307
+ }));
308
+ })
309
+ })));
310
+ const runtime = yield* spawnGeneration(machineWithState);
311
+ runtimeRef.current = runtime;
312
+ const supervision = options?.supervision;
313
+ let supervisorFiber;
314
+ if (supervision !== void 0) supervisorFiber = yield* Effect.forkDetach(Effect.gen(function* () {
315
+ const step = yield* Schedule.toStepWithSleep(supervision.schedule);
316
+ let generation = 0;
317
+ while (true) {
318
+ const currentRuntime = runtimeRef.current;
319
+ if (currentRuntime === void 0) return;
320
+ const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
321
+ if (generationExit._tag !== "Defect") {
322
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
323
+ return;
324
+ }
325
+ if (supervision.shouldRestart !== void 0 && !supervision.shouldRestart(generationExit)) {
326
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
327
+ return;
328
+ }
329
+ if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
330
+ yield* Deferred.succeed(terminalExitDeferred, generationExit);
331
+ return;
332
+ }
333
+ yield* settlePendingReplies(pendingReplies, id);
334
+ const freshQueue = yield* Queue.unbounded();
335
+ yield* Ref.set(eventQueueRef, freshQueue);
336
+ yield* SubscriptionRef.set(stateRef, machine.initial);
337
+ yield* Ref.set(stoppedRef, false);
338
+ childrenMap.clear();
339
+ runtimeRef.current = yield* spawnGeneration(machine);
340
+ generation++;
341
+ if (options?.onRestart !== void 0) yield* options.onRestart(generation, generationExit);
342
+ notifyListeners(listeners, machine.initial);
343
+ }
344
+ }));
345
+ else yield* Effect.forkDetach(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
346
+ return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
347
+ if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
348
+ const currentRuntime = runtimeRef.current;
349
+ if (currentRuntime !== void 0) yield* currentRuntime.stop;
350
+ yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
257
351
  if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
258
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
352
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
259
353
  });
260
354
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
261
355
  const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
@@ -263,207 +357,6 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
263
357
  for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
264
358
  pendingReplies.clear();
265
359
  });
266
- /**
267
- * Main event loop for the actor.
268
- * Includes postpone buffer — events matching postpone rules are buffered
269
- * and drained after state tag changes (gen_statem semantics).
270
- */
271
- const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub, deferredReplyRef) {
272
- const postponed = [];
273
- const hasPostponeRules = machine.postponeRules.length > 0;
274
- const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
275
- const event = queued.event;
276
- const currentState = yield* SubscriptionRef.get(stateRef);
277
- if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
278
- postponed.push(queued);
279
- if (queued._tag === "call") {
280
- const postponedResult = {
281
- newState: currentState,
282
- previousState: currentState,
283
- transitioned: false,
284
- lifecycleRan: false,
285
- isFinal: false,
286
- hasReply: false,
287
- deferReply: false,
288
- reply: void 0,
289
- postponed: true
290
- };
291
- yield* Deferred.succeed(queued.reply, postponedResult);
292
- }
293
- return {
294
- shouldStop: false,
295
- stateChanged: false
296
- };
297
- }
298
- const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
299
- "effect_machine.actor.id": actorId,
300
- "effect_machine.state.current": currentState._tag,
301
- "effect_machine.event.type": event._tag
302
- } })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
303
- switch (queued._tag) {
304
- case "call":
305
- yield* Deferred.succeed(queued.reply, result);
306
- break;
307
- case "ask":
308
- if (result.hasReply) {
309
- const replySchema = machine._replySchemas?.get(event._tag);
310
- if (replySchema !== void 0) {
311
- let decoded;
312
- try {
313
- decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
314
- } catch (decodeError) {
315
- yield* Deferred.die(queued.reply, decodeError);
316
- return yield* Effect.die(decodeError);
317
- }
318
- yield* Deferred.succeed(queued.reply, decoded);
319
- } else yield* Deferred.succeed(queued.reply, result.reply);
320
- } else if (result.deferReply) deferredReplyRef.current = queued.reply;
321
- else yield* Deferred.fail(queued.reply, new NoReplyError({
322
- actorId,
323
- eventTag: event._tag
324
- }));
325
- break;
326
- }
327
- if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
328
- fromState: result.previousState,
329
- toState: result.newState,
330
- event
331
- });
332
- return {
333
- shouldStop,
334
- stateChanged: result.lifecycleRan
335
- };
336
- });
337
- while (true) {
338
- const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
339
- if (shouldStop) {
340
- yield* Ref.set(stoppedRef, true);
341
- settlePostponedBuffer(postponed, pendingReplies, actorId);
342
- yield* settlePendingReplies(pendingReplies, actorId);
343
- yield* Scope.close(stateScopeRef.current, Exit.void);
344
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
345
- return;
346
- }
347
- let drainTriggered = stateChanged;
348
- while (drainTriggered && postponed.length > 0) {
349
- drainTriggered = false;
350
- const drained = postponed.splice(0);
351
- for (const entry of drained) {
352
- const drain = yield* processQueued(entry);
353
- if (drain.shouldStop) {
354
- yield* Ref.set(stoppedRef, true);
355
- settlePostponedBuffer(postponed, pendingReplies, actorId);
356
- yield* settlePendingReplies(pendingReplies, actorId);
357
- yield* Scope.close(stateScopeRef.current, Exit.void);
358
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
359
- return;
360
- }
361
- if (drain.stateChanged) drainTriggered = true;
362
- }
363
- }
364
- }
365
- });
366
- /**
367
- * Settle all reply-bearing entries in the postpone buffer on shutdown.
368
- * Call entries already had their Deferred settled with the postponed result
369
- * (so their pendingReplies entry is already removed). Ask/send entries
370
- * with Deferreds are settled via the pendingReplies registry.
371
- */
372
- const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
373
- postponed.length = 0;
374
- };
375
- /**
376
- * Process a single event, returning true if the actor should stop.
377
- * Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
378
- */
379
- const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
380
- yield* emitWithTimestamp(inspector, (timestamp) => ({
381
- type: "@machine.event",
382
- actorId,
383
- state: currentState,
384
- event,
385
- timestamp
386
- }));
387
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
388
- onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
389
- type: "@machine.effect",
390
- actorId,
391
- effectType: "spawn",
392
- state,
393
- timestamp
394
- })),
395
- onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
396
- type: "@machine.transition",
397
- actorId,
398
- fromState: from,
399
- toState: to,
400
- event: ev,
401
- timestamp
402
- })),
403
- onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
404
- type: "@machine.error",
405
- actorId,
406
- phase: info.phase,
407
- state: info.state,
408
- event: info.event,
409
- error: Cause.pretty(info.cause),
410
- timestamp
411
- }))
412
- });
413
- if (!result.transitioned) {
414
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
415
- return {
416
- shouldStop: false,
417
- result
418
- };
419
- }
420
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
421
- yield* SubscriptionRef.set(stateRef, result.newState);
422
- notifyListeners(listeners, result.newState);
423
- if (result.lifecycleRan) {
424
- yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
425
- yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
426
- if (result.isFinal) {
427
- yield* emitWithTimestamp(inspector, (timestamp) => ({
428
- type: "@machine.stop",
429
- actorId,
430
- finalState: result.newState,
431
- timestamp
432
- }));
433
- return {
434
- shouldStop: true,
435
- result
436
- };
437
- }
438
- }
439
- return {
440
- shouldStop: false,
441
- result
442
- };
443
- });
444
- /**
445
- * Run spawn effects with actor-specific inspection and tracing.
446
- * Wraps the core runSpawnEffects with inspection events and spans.
447
- * @internal
448
- */
449
- const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
450
- yield* emitWithTimestamp(inspector, (timestamp) => ({
451
- type: "@machine.effect",
452
- actorId,
453
- effectType: "spawn",
454
- state,
455
- timestamp
456
- }));
457
- yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
458
- type: "@machine.error",
459
- actorId,
460
- phase: info.phase,
461
- state: info.state,
462
- event: info.event,
463
- error: Cause.pretty(info.cause),
464
- timestamp
465
- })));
466
- });
467
360
  /** Notify all system event listeners (sync). */
468
361
  const notifySystemListeners = (listeners, event) => {
469
362
  for (const listener of listeners) try {
@@ -502,7 +395,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
502
395
  yield* emitSystemEvent({
503
396
  _tag: "ActorStopped",
504
397
  id,
505
- actor: actorRef
398
+ actor: actorRef,
399
+ exit: { _tag: "Stopped" }
506
400
  });
507
401
  MutableHashMap.remove(actorsMap, id);
508
402
  }
@@ -510,11 +404,23 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
510
404
  }));
511
405
  return actor;
512
406
  });
513
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
407
+ const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
514
408
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
515
- return yield* registerActor(id, yield* createActor(id, built._inner));
409
+ let actorRef;
410
+ const actor = yield* createActor(id, machine, {
411
+ supervision: spawnOptions?.supervision,
412
+ onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
413
+ _tag: "ActorRestarted",
414
+ id,
415
+ actor: actorRef,
416
+ generation,
417
+ exit
418
+ }) : Effect.void : void 0
419
+ });
420
+ actorRef = actor;
421
+ return yield* registerActor(id, actor);
516
422
  });
517
- const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
423
+ const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
518
424
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
519
425
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
520
426
  });
@@ -526,7 +432,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
526
432
  yield* emitSystemEvent({
527
433
  _tag: "ActorStopped",
528
434
  id,
529
- actor
435
+ actor,
436
+ exit: { _tag: "Stopped" }
530
437
  });
531
438
  yield* actor.stop;
532
439
  return true;