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