effect-machine 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +133 -324
  2. package/dist/actor.d.ts +46 -28
  3. package/dist/actor.js +276 -315
  4. package/dist/cluster/entity-machine.d.ts +1 -1
  5. package/dist/cluster/entity-machine.js +20 -9
  6. package/dist/cluster/to-entity.d.ts +3 -3
  7. package/dist/errors.d.ts +24 -20
  8. package/dist/errors.js +10 -6
  9. package/dist/index.d.ts +5 -4
  10. package/dist/index.js +3 -2
  11. package/dist/internal/runtime.d.ts +82 -7
  12. package/dist/internal/runtime.js +162 -58
  13. package/dist/internal/transition.d.ts +12 -11
  14. package/dist/internal/transition.js +12 -14
  15. package/dist/machine.d.ts +148 -140
  16. package/dist/machine.js +141 -155
  17. package/dist/schema.d.ts +14 -0
  18. package/dist/schema.js +10 -1
  19. package/dist/slot.d.ts +112 -86
  20. package/dist/slot.js +92 -59
  21. package/dist/supervision.d.ts +97 -0
  22. package/dist/supervision.js +42 -0
  23. package/dist/testing.d.ts +21 -12
  24. package/dist/testing.js +23 -26
  25. package/package.json +7 -7
  26. package/v3/dist/actor.d.ts +53 -30
  27. package/v3/dist/actor.js +286 -311
  28. package/v3/dist/cluster/entity-machine.d.ts +1 -1
  29. package/v3/dist/cluster/entity-machine.js +5 -5
  30. package/v3/dist/cluster/to-entity.d.ts +1 -1
  31. package/v3/dist/errors.d.ts +14 -10
  32. package/v3/dist/errors.js +11 -7
  33. package/v3/dist/index.d.ts +6 -5
  34. package/v3/dist/index.js +3 -2
  35. package/v3/dist/inspection.d.ts +3 -22
  36. package/v3/dist/inspection.js +1 -15
  37. package/v3/dist/internal/brands.d.ts +4 -8
  38. package/v3/dist/internal/inspection.js +1 -1
  39. package/v3/dist/internal/runtime.d.ts +87 -10
  40. package/v3/dist/internal/runtime.js +177 -61
  41. package/v3/dist/internal/transition.d.ts +13 -12
  42. package/v3/dist/internal/transition.js +14 -16
  43. package/v3/dist/internal/utils.js +5 -1
  44. package/v3/dist/machine.d.ts +158 -143
  45. package/v3/dist/machine.js +148 -155
  46. package/v3/dist/schema.d.ts +25 -11
  47. package/v3/dist/schema.js +18 -5
  48. package/v3/dist/slot.d.ts +112 -86
  49. package/v3/dist/slot.js +92 -59
  50. package/v3/dist/supervision.d.ts +97 -0
  51. package/v3/dist/supervision.js +42 -0
  52. package/v3/dist/testing.d.ts +21 -12
  53. package/v3/dist/testing.js +23 -24
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.
@@ -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)),
@@ -146,314 +163,251 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
146
163
  children: childrenMap
147
164
  };
148
165
  };
166
+ /** Build ProcessEventHooks from an inspector */
167
+ const buildInspectionHooks = (actorId, inspector) => ({
168
+ onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
169
+ type: "@machine.effect",
170
+ actorId,
171
+ effectType: "spawn",
172
+ state,
173
+ timestamp
174
+ })),
175
+ onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
176
+ type: "@machine.transition",
177
+ actorId,
178
+ fromState: from,
179
+ toState: to,
180
+ event: ev,
181
+ timestamp
182
+ })),
183
+ onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
184
+ type: "@machine.error",
185
+ actorId,
186
+ phase: info.phase,
187
+ state: info.state,
188
+ event: info.event,
189
+ error: Cause.pretty(info.cause),
190
+ timestamp
191
+ }))
192
+ });
149
193
  /**
150
- * Create and start an actor for a machine
194
+ * Load persisted state and run onRestore hook if present.
195
+ * Returns the resolved initial state (loaded, restored, or fallback to machineInitial).
196
+ * @internal
151
197
  */
152
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
153
- const initial = options?.initialState ?? machine.initial;
154
- yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
198
+ const loadAndRestore = (persist, machineInitial) => Effect.gen(function* () {
199
+ const loaded = yield* persist.load();
200
+ if (Option.isNone(loaded)) return machineInitial;
201
+ if (persist.onRestore === void 0) return loaded.value;
202
+ const restored = yield* persist.onRestore(loaded.value, { initial: machineInitial });
203
+ return Option.getOrElse(restored, () => machineInitial);
204
+ });
205
+ /**
206
+ * Resolve actor system from context, creating an implicit one if none exists.
207
+ * @internal
208
+ */
209
+ const resolveActorSystem = Effect.fn("effect-machine.resolveActorSystem")(function* () {
155
210
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
156
- let system;
157
- let implicitSystemScope;
158
- if (Option.isSome(existingSystem)) system = existingSystem.value;
159
- else {
160
- const scope = yield* Scope.make();
161
- system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
162
- implicitSystemScope = scope;
163
- }
164
- const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
165
- const eventQueue = yield* Queue.unbounded();
166
- const stoppedRef = yield* Ref.make(false);
167
- 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
- })
211
+ if (Option.isSome(existingSystem)) return {
212
+ system: existingSystem.value,
213
+ implicitSystemScope: void 0
187
214
  };
188
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
189
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
190
- type: "@machine.spawn",
191
- actorId: id,
192
- initialState: initial,
193
- timestamp
194
- }));
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
215
+ const scope = yield* Scope.make();
216
+ return {
217
+ system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
218
+ implicitSystemScope: scope
205
219
  };
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
220
  });
258
221
  /**
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).
222
+ * Run the supervision loop for a supervised actor.
223
+ * Observes exit deferred, applies restart policy, resets cell resources on restart.
224
+ * @internal
262
225
  */
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
- });
226
+ const runSupervisionLoop = (params) => Effect.gen(function* () {
227
+ const driver = yield* Schedule.driver(params.supervision.schedule);
228
+ let generation = 0;
328
229
  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" });
230
+ const currentRuntime = params.runtimeRef.current;
231
+ if (currentRuntime === void 0) return;
232
+ const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
233
+ if (generationExit._tag !== "Defect") {
234
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
336
235
  return;
337
236
  }
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
- }
237
+ if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
238
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
239
+ return;
354
240
  }
241
+ if ((yield* driver.next(generationExit).pipe(Effect.exit))._tag === "Failure") {
242
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
243
+ return;
244
+ }
245
+ const restartState = params.persist !== void 0 ? yield* loadAndRestore(params.persist, params.machine.initial) : params.machine.initial;
246
+ yield* settlePendingReplies(params.pendingReplies, params.id);
247
+ const freshQueue = yield* Queue.unbounded();
248
+ yield* Ref.set(params.eventQueueRef, freshQueue);
249
+ yield* SubscriptionRef.set(params.stateRef, restartState);
250
+ yield* Ref.set(params.stoppedRef, false);
251
+ params.childrenMap.clear();
252
+ const machineForRestart = restartState !== params.machine.initial ? Object.create(params.machine, { initial: {
253
+ value: restartState,
254
+ enumerable: true
255
+ } }) : params.machine;
256
+ const newRuntime = yield* params.spawnGeneration(machineForRestart);
257
+ params.runtimeRef.current = newRuntime;
258
+ generation++;
259
+ if (params.onRestart !== void 0) yield* params.onRestart(generation, generationExit);
260
+ notifyListeners(params.listeners, restartState);
355
261
  }
356
262
  });
357
263
  /**
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).
264
+ * Create and start an actor for a machine.
265
+ * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
369
266
  */
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,
267
+ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
268
+ const persist = options?.persist;
269
+ const initial = options?.initialState ?? (persist !== void 0 ? yield* loadAndRestore(persist, machine.initial) : machine.initial);
270
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
271
+ yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
272
+ const { system, implicitSystemScope } = yield* resolveActorSystem();
273
+ const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
274
+ const childrenMap = /* @__PURE__ */ new Map();
275
+ const pendingReplies = /* @__PURE__ */ new Set();
276
+ const listeners = /* @__PURE__ */ new Set();
277
+ const transitionsPubSub = yield* PubSub.unbounded();
278
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
279
+ type: "@machine.spawn",
280
+ actorId: id,
281
+ initialState: initial,
376
282
  timestamp
377
283
  }));
378
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
379
- onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
380
- type: "@machine.effect",
381
- actorId,
382
- effectType: "spawn",
383
- state,
384
- timestamp
385
- })),
386
- onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
387
- type: "@machine.transition",
388
- actorId,
389
- fromState: from,
390
- toState: to,
391
- event: ev,
392
- timestamp
393
- })),
394
- onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
395
- type: "@machine.error",
396
- actorId,
397
- phase: info.phase,
398
- state: info.state,
399
- event: info.event,
400
- error: Cause.pretty(info.cause),
401
- timestamp
402
- }))
403
- });
404
- if (!result.transitioned) {
405
- yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
284
+ const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
285
+ const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
286
+ value: initial,
287
+ enumerable: true
288
+ } }) : machine;
289
+ const stateRef = yield* SubscriptionRef.make(initial);
290
+ const stoppedRef = yield* Ref.make(false);
291
+ const initialQueue = yield* Queue.unbounded();
292
+ const eventQueueRef = yield* Ref.make(initialQueue);
293
+ const terminalExitDeferred = yield* Deferred.make();
294
+ let stopEmitted = false;
295
+ const runtimeRef = { current: void 0 };
296
+ /** Build lifecycle hooks for a generation */
297
+ const buildLifecycle = () => {
298
+ stopEmitted = false;
406
299
  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,
300
+ onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
301
+ type: "@machine.event",
302
+ actorId: id,
303
+ state,
304
+ event,
422
305
  timestamp
423
- }));
424
- return {
425
- shouldStop: true,
426
- result
427
- };
428
- }
429
- }
430
- return {
431
- shouldStop: false,
432
- result
306
+ })) : void 0,
307
+ onStateChange: (result, _event) => Effect.gen(function* () {
308
+ notifyListeners(listeners, result.newState);
309
+ if (persist !== void 0 && result.transitioned) {
310
+ if (persist.shouldSave === void 0 || persist.shouldSave(result.newState, result.previousState)) yield* persist.save(result.newState);
311
+ }
312
+ yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
313
+ if (result.lifecycleRan) {
314
+ yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
315
+ yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
316
+ }
317
+ }),
318
+ onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
319
+ fromState: result.previousState,
320
+ toState: result.newState,
321
+ event
322
+ }).pipe(Effect.asVoid) : Effect.void,
323
+ onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
324
+ stopEmitted = true;
325
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
326
+ type: "@machine.stop",
327
+ actorId: id,
328
+ finalState: state,
329
+ timestamp
330
+ }));
331
+ }) : void 0,
332
+ onShutdown: () => Effect.gen(function* () {
333
+ if (!stopEmitted) {
334
+ const finalState = yield* SubscriptionRef.get(stateRef);
335
+ yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
336
+ type: "@machine.stop",
337
+ actorId: id,
338
+ finalState,
339
+ timestamp
340
+ }));
341
+ }
342
+ yield* settlePendingReplies(pendingReplies, id);
343
+ }),
344
+ onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
345
+ type: "@machine.effect",
346
+ actorId: id,
347
+ effectType: "spawn",
348
+ state,
349
+ timestamp
350
+ })) : void 0
351
+ };
433
352
  };
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
- }));
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
353
+ /** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
354
+ const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
355
+ actorId: id,
356
+ hooks,
357
+ skipFinalizer: true,
358
+ cellResources: {
359
+ stateRef,
360
+ stoppedRef,
361
+ eventQueue: currentQueue
362
+ },
363
+ lifecycle: buildLifecycle(),
364
+ wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
365
+ "effect_machine.actor.id": id,
366
+ "effect_machine.state.current": state._tag,
367
+ "effect_machine.event.type": event._tag
368
+ } })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
369
+ onChildSpawned: (childId, child) => Effect.gen(function* () {
370
+ childrenMap.set(childId, child);
371
+ const maybeScope = yield* Effect.serviceOption(Scope.Scope);
372
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
373
+ childrenMap.delete(childId);
374
+ }));
375
+ })
456
376
  })));
377
+ const runtime = yield* spawnGeneration(machineWithState);
378
+ runtimeRef.current = runtime;
379
+ const supervision = options?.supervision;
380
+ let supervisorFiber;
381
+ if (supervision !== void 0) supervisorFiber = yield* Effect.forkDaemon(runSupervisionLoop({
382
+ supervision,
383
+ machine,
384
+ id,
385
+ runtimeRef,
386
+ terminalExitDeferred,
387
+ pendingReplies,
388
+ eventQueueRef,
389
+ stateRef,
390
+ stoppedRef,
391
+ childrenMap,
392
+ listeners,
393
+ spawnGeneration,
394
+ persist,
395
+ onRestart: options?.onRestart
396
+ }));
397
+ else yield* Effect.forkDaemon(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
398
+ return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
399
+ if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
400
+ const currentRuntime = runtimeRef.current;
401
+ if (currentRuntime !== void 0) yield* currentRuntime.stop;
402
+ yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
403
+ if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
404
+ }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
405
+ });
406
+ /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
407
+ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
408
+ const error = new ActorStoppedError({ actorId });
409
+ for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
410
+ pendingReplies.clear();
457
411
  });
458
412
  /** Notify all system event listeners (sync). */
459
413
  const notifySystemListeners = (listeners, event) => {
@@ -466,13 +420,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
466
420
  const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
467
421
  const eventPubSub = yield* PubSub.unbounded();
468
422
  const eventListeners = /* @__PURE__ */ new Set();
469
- const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.zipRight(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
423
+ const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
470
424
  yield* Effect.addFinalizer(() => {
471
425
  const stops = [];
472
426
  MutableHashMap.forEach(actorsMap, (actor) => {
473
427
  stops.push(actor.stop);
474
428
  });
475
- return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.zipRight(PubSub.shutdown(eventPubSub)), Effect.asVoid);
429
+ return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
476
430
  });
477
431
  /** Check for duplicate ID, register actor, attach scope cleanup if available */
478
432
  const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
@@ -493,7 +447,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
493
447
  yield* emitSystemEvent({
494
448
  _tag: "ActorStopped",
495
449
  id,
496
- actor: actorRef
450
+ actor: actorRef,
451
+ exit: { _tag: "Stopped" }
497
452
  });
498
453
  MutableHashMap.remove(actorsMap, id);
499
454
  }
@@ -501,11 +456,25 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
501
456
  }));
502
457
  return actor;
503
458
  });
504
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
459
+ const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
505
460
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
506
- return yield* registerActor(id, yield* createActor(id, built._inner));
461
+ const materialized = spawnOptions?.slots !== void 0 ? materializeMachine(machine, spawnOptions.slots) : machine;
462
+ let actorRef;
463
+ const actor = yield* createActor(id, materialized, {
464
+ supervision: spawnOptions?.supervision,
465
+ persist: spawnOptions?.persist,
466
+ onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
467
+ _tag: "ActorRestarted",
468
+ id,
469
+ actor: actorRef,
470
+ generation,
471
+ exit
472
+ }) : Effect.void : void 0
473
+ });
474
+ actorRef = actor;
475
+ return yield* registerActor(id, actor);
507
476
  });
508
- const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
477
+ const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
509
478
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
510
479
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
511
480
  });
@@ -517,7 +486,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
517
486
  yield* emitSystemEvent({
518
487
  _tag: "ActorStopped",
519
488
  id,
520
- actor
489
+ actor,
490
+ exit: { _tag: "Stopped" }
521
491
  });
522
492
  yield* actor.stop;
523
493
  return true;
@@ -543,8 +513,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
543
513
  });
544
514
  });
545
515
  /**
516
+ * Create an ActorSystem instance. Must be run in a Scope.
517
+ * @internal — use Default layer for normal usage
518
+ */
519
+ const makeSystem = make;
520
+ /**
546
521
  * Default ActorSystem layer
547
522
  */
548
523
  const Default = Layer.scoped(ActorSystem, make());
549
524
  //#endregion
550
- export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
525
+ export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };