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/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, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schema, Scope, Semaphore, ServiceMap, 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, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
7
8
  //#region src/actor.ts
8
9
  /**
9
10
  * Actor system: spawning, lifecycle, and event processing.
@@ -28,10 +29,11 @@ const notifyListeners = (listeners, state) => {
28
29
  /**
29
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
@@ -126,12 +130,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
126
130
  listeners.delete(fn);
127
131
  };
128
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),
129
145
  sync: {
130
146
  send: (event) => {
131
- if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
132
- _tag: "send",
133
- event
134
- }));
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
+ }
135
154
  },
136
155
  stop: () => Effect.runFork(stop),
137
156
  snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
@@ -144,325 +163,251 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
144
163
  children: childrenMap
145
164
  };
146
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
+ });
147
193
  /**
148
- * 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
149
197
  */
150
- const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
151
- const initial = options?.initialState ?? machine.initial;
152
- 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* () {
153
210
  const existingSystem = yield* Effect.serviceOption(ActorSystem);
154
- let system;
155
- let implicitSystemScope;
156
- if (Option.isSome(existingSystem)) system = existingSystem.value;
157
- else {
158
- const scope = yield* Scope.make();
159
- system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
160
- implicitSystemScope = scope;
161
- }
162
- const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
163
- const eventQueue = yield* Queue.unbounded();
164
- const stoppedRef = yield* Ref.make(false);
165
- 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
- })
211
+ if (Option.isSome(existingSystem)) return {
212
+ system: existingSystem.value,
213
+ implicitSystemScope: void 0
195
214
  };
196
- yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
197
- yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
198
- type: "@machine.spawn",
199
- actorId: id,
200
- initialState: initial,
201
- timestamp
202
- }));
203
- 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
215
+ const scope = yield* Scope.make();
216
+ return {
217
+ system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
218
+ implicitSystemScope: scope
213
219
  };
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" });
257
- if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
258
- }).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
259
- });
260
- /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
261
- const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
262
- const error = new ActorStoppedError({ actorId });
263
- for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
264
- pendingReplies.clear();
265
220
  });
266
221
  /**
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).
222
+ * Run the supervision loop for a supervised actor.
223
+ * Observes exit deferred, applies restart policy, resets cell resources on restart.
224
+ * @internal
270
225
  */
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
- });
226
+ const runSupervisionLoop = (params) => Effect.gen(function* () {
227
+ const step = yield* Schedule.toStepWithSleep(params.supervision.schedule);
228
+ let generation = 0;
337
229
  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" });
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);
345
235
  return;
346
236
  }
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
- }
237
+ if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
238
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
239
+ return;
240
+ }
241
+ if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
242
+ yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
243
+ return;
363
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);
364
261
  }
365
262
  });
366
263
  /**
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).
264
+ * Create and start an actor for a machine.
265
+ * Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
378
266
  */
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,
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,
385
282
  timestamp
386
283
  }));
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);
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;
415
299
  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,
300
+ onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
301
+ type: "@machine.event",
302
+ actorId: id,
303
+ state,
304
+ event,
431
305
  timestamp
432
- }));
433
- return {
434
- shouldStop: true,
435
- result
436
- };
437
- }
438
- }
439
- return {
440
- shouldStop: false,
441
- 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
+ };
442
352
  };
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
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
+ })
465
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.forkDetach(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.forkDetach(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();
466
411
  });
467
412
  /** Notify all system event listeners (sync). */
468
413
  const notifySystemListeners = (listeners, event) => {
@@ -502,7 +447,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
502
447
  yield* emitSystemEvent({
503
448
  _tag: "ActorStopped",
504
449
  id,
505
- actor: actorRef
450
+ actor: actorRef,
451
+ exit: { _tag: "Stopped" }
506
452
  });
507
453
  MutableHashMap.remove(actorsMap, id);
508
454
  }
@@ -510,11 +456,25 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
510
456
  }));
511
457
  return actor;
512
458
  });
513
- const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, built) {
459
+ const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
514
460
  if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
515
- 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);
516
476
  });
517
- const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
477
+ const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
518
478
  const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
519
479
  return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
520
480
  });
@@ -526,7 +486,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
526
486
  yield* emitSystemEvent({
527
487
  _tag: "ActorStopped",
528
488
  id,
529
- actor
489
+ actor,
490
+ exit: { _tag: "Stopped" }
530
491
  });
531
492
  yield* actor.stop;
532
493
  return true;