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
@@ -1,8 +1,9 @@
1
1
  import { INTERNAL_INIT_EVENT } from "./utils.js";
2
2
  import { NoReplyError } from "../errors.js";
3
3
  import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
4
+ import { ActorExit } from "../supervision.js";
4
5
  import { ActorSystem } from "../actor.js";
5
- import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Runtime, Schema, Scope, SubscriptionRef } from "effect";
6
7
  //#region src/internal/runtime.ts
7
8
  /**
8
9
  * Shared runtime kernel for machine event processing.
@@ -10,45 +11,65 @@ import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect
10
11
  * Provides a single-queue event loop with:
11
12
  * - Sequential event processing (no split-mailbox race)
12
13
  * - Postpone buffer with drain-on-state-change (gen_statem)
13
- * - Background effect lifecycle
14
+ * - Background effect lifecycle (under actorScope fault boundary)
14
15
  * - Spawn effect lifecycle (per-state scope)
15
16
  * - Final state detection → stop
16
17
  * - Reply settlement (call/ask Deferreds)
17
18
  * - Reply schema validation
19
+ * - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
20
+ * - ActorExit with exit reason (Final/Stopped/Defect) via exitDeferred
18
21
  *
19
- * Used by entity-machine. Local actor (actor.ts) has its own event loop
20
- * with additional concerns (inspection, listeners, subscription ref, etc.)
21
- * that will be migrated to use this kernel in a future refactor.
22
+ * Used by entity-machine and local actor (actor.ts delegates here).
22
23
  *
23
24
  * @internal
24
25
  */
25
26
  /**
26
27
  * Create a runtime for a machine. Returns a handle for sending events
27
28
  * and querying state. The runtime owns:
28
- * - Single event queue (all events serialized)
29
29
  * - Event loop fiber
30
30
  * - Postpone buffer
31
- * - Background effects
31
+ * - Background effects (under actorScope)
32
32
  * - State scope (spawn effects)
33
33
  * - Final state detection
34
+ * - Exit reason via exitDeferred
35
+ *
36
+ * Resources (stateRef, eventQueue, stoppedRef) are either cell-provided
37
+ * or allocated fresh by the runtime.
34
38
  *
35
39
  * @internal
36
40
  */
37
41
  const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
38
- const { actorId, hooks } = config;
39
- const stateRef = yield* Ref.make(machine.initial);
40
- const stoppedRef = yield* Ref.make(false);
41
- const eventQueue = yield* config.queueFactory ?? Queue.unbounded();
42
+ const { actorId, hooks, lifecycle } = config;
43
+ const rt = yield* Effect.runtime();
44
+ const fork = Runtime.runFork(rt);
45
+ const stateRef = config.cellResources?.stateRef ?? (yield* SubscriptionRef.make(machine.initial));
46
+ const stoppedRef = config.cellResources?.stoppedRef ?? (yield* Ref.make(false));
47
+ const eventQueue = config.cellResources?.eventQueue ?? (yield* config.queueFactory ?? Queue.unbounded());
48
+ const exitDeferred = yield* Deferred.make();
49
+ const actorScope = yield* Scope.make();
50
+ const deferredReplyRef = { current: void 0 };
42
51
  const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
43
52
  if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
44
53
  _tag: "send",
45
54
  event
46
55
  });
47
56
  });
57
+ const childPrefix = config.childIdPrefix ?? "";
58
+ const defaultSpawn = (childId, childMachine) => system.spawn(`${childPrefix}${childId}`, childMachine).pipe(Effect.provideService(ActorSystem, system));
59
+ const onChildSpawned = config.onChildSpawned;
48
60
  const self = {
49
61
  send: selfSend,
50
62
  cast: selfSend,
51
- spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
63
+ spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn,
64
+ reply: (value) => Effect.sync(() => {
65
+ const deferred = deferredReplyRef.current;
66
+ if (deferred !== void 0) {
67
+ deferredReplyRef.current = void 0;
68
+ fork(Deferred.succeed(deferred, value));
69
+ return true;
70
+ }
71
+ return false;
72
+ })
52
73
  };
53
74
  const stateScopeRef = { current: yield* Scope.make() };
54
75
  const backgroundFibers = [];
@@ -60,36 +81,67 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
60
81
  self,
61
82
  system
62
83
  };
63
- const { effects: effectSlots } = machine._slots;
84
+ const slots = machine._slots;
64
85
  for (const bg of machine.backgroundEffects) {
65
- const fiber = yield* Effect.forkDaemon(bg.handler({
86
+ const fiber = yield* bg.handler({
66
87
  actorId,
67
88
  state: machine.initial,
68
89
  event: initEvent,
69
90
  self,
70
- effects: effectSlots,
91
+ slots,
71
92
  system
72
- }).pipe(Effect.provideService(machine.Context, ctx)));
93
+ }).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
73
94
  backgroundFibers.push(fiber);
74
95
  }
75
- yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError);
96
+ if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
97
+ const loopFiberRef = { current: void 0 };
98
+ const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid);
99
+ yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
100
+ return Effect.gen(function* () {
101
+ yield* Ref.set(stoppedRef, true);
102
+ yield* Scope.close(stateScopeRef.current, Exit.void);
103
+ yield* Scope.close(actorScope, Exit.void);
104
+ yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
105
+ return yield* Effect.failCause(cause);
106
+ });
107
+ }));
108
+ /** Set the exit deferred exactly once. */
109
+ const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
76
110
  if (machine.finalStates.has(machine.initial._tag)) {
111
+ if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
77
112
  yield* Ref.set(stoppedRef, true);
78
113
  yield* Scope.close(stateScopeRef.current, Exit.void);
79
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
80
- return makeHandle(stateRef, stoppedRef, eventQueue, machine);
114
+ yield* Scope.close(actorScope, Exit.void);
115
+ yield* setExit(ActorExit.Final(machine.initial));
116
+ return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
81
117
  }
82
- const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks));
118
+ const augmentedHooks = {
119
+ ...hooks,
120
+ onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid)
121
+ };
122
+ const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
123
+ loopFiberRef.current = loopFiber;
124
+ if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
125
+ if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
126
+ return Effect.never;
127
+ })))).pipe(Effect.forkIn(actorScope));
128
+ yield* Effect.forkDaemon(Effect.gen(function* () {
129
+ const loopExit = yield* Fiber.await(loopFiber);
130
+ if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
131
+ else yield* Scope.close(actorScope, loopExit);
132
+ }));
83
133
  const stop = Effect.gen(function* () {
84
134
  if (yield* Ref.get(stoppedRef)) return;
135
+ if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
85
136
  yield* Ref.set(stoppedRef, true);
86
137
  yield* Fiber.interrupt(loopFiber);
87
138
  yield* Scope.close(stateScopeRef.current, Exit.void);
88
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
139
+ yield* Scope.close(actorScope, Exit.void);
140
+ yield* setExit(ActorExit.Stopped);
89
141
  }).pipe(Effect.asVoid);
90
- yield* Effect.addFinalizer(() => stop);
142
+ if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
91
143
  return {
92
- ...makeHandle(stateRef, stoppedRef, eventQueue, machine),
144
+ ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
93
145
  stop
94
146
  };
95
147
  });
@@ -97,7 +149,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
97
149
  * Build the runtime handle (send/ask/getState/isStopped).
98
150
  * Shared between initial-final and normal paths.
99
151
  */
100
- const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
152
+ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
101
153
  send: (event) => Effect.gen(function* () {
102
154
  if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
103
155
  _tag: "send",
@@ -128,30 +180,68 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
128
180
  });
129
181
  return yield* Deferred.await(reply);
130
182
  }),
131
- getState: Ref.get(stateRef),
183
+ getState: SubscriptionRef.get(stateRef),
184
+ stateRef,
132
185
  isStopped: Ref.get(stoppedRef),
133
- stop: Effect.void
186
+ stop: Effect.void,
187
+ _queue: eventQueue,
188
+ _stoppedRef: stoppedRef,
189
+ exitDeferred,
190
+ actorScope
134
191
  });
135
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks) {
192
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, wrapProcess, fork) {
193
+ const forkEffect = fork ?? Effect.runFork;
194
+ /** Set the exit deferred exactly once. */
195
+ const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
136
196
  const postponed = [];
137
197
  const hasPostponeRules = machine.postponeRules.length > 0;
138
198
  const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
139
199
  const event = queued.event;
140
- const currentState = yield* Ref.get(stateRef);
200
+ const currentState = yield* SubscriptionRef.get(stateRef);
141
201
  if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
202
+ if (queued._tag === "call") {
203
+ const postponedResult = {
204
+ newState: currentState,
205
+ previousState: currentState,
206
+ transitioned: false,
207
+ lifecycleRan: false,
208
+ isFinal: false,
209
+ hasReply: false,
210
+ deferReply: false,
211
+ reply: void 0,
212
+ postponed: true
213
+ };
214
+ yield* Deferred.succeed(queued.reply, postponedResult);
215
+ }
216
+ if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
142
217
  postponed.push({
143
218
  _tag: "send",
144
219
  event
145
220
  });
146
- if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
147
221
  return {
148
222
  shouldStop: false,
149
- stateChanged: false
223
+ stateChanged: false,
224
+ result: {
225
+ newState: currentState,
226
+ previousState: currentState,
227
+ transitioned: false,
228
+ lifecycleRan: false,
229
+ isFinal: false,
230
+ hasReply: false,
231
+ deferReply: false,
232
+ reply: void 0,
233
+ postponed: true
234
+ }
150
235
  };
151
236
  }
237
+ if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
152
238
  const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
153
- if (result.transitioned) yield* Ref.set(stateRef, result.newState);
239
+ if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
240
+ if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
154
241
  switch (queued._tag) {
242
+ case "call":
243
+ yield* Deferred.succeed(queued.reply, result);
244
+ break;
155
245
  case "sendWait":
156
246
  yield* Deferred.succeed(queued.done, void 0);
157
247
  break;
@@ -168,35 +258,69 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
168
258
  }
169
259
  yield* Deferred.succeed(queued.reply, decoded);
170
260
  } else yield* Deferred.succeed(queued.reply, result.reply);
171
- } else yield* Deferred.fail(queued.reply, new NoReplyError({
261
+ } else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
262
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
172
263
  actorId,
173
264
  eventTag: event._tag
174
265
  }));
175
266
  break;
176
267
  }
268
+ if (lifecycle?.onProcessed !== void 0 && result.transitioned) yield* lifecycle.onProcessed(result, event);
269
+ const shouldStop = result.isFinal && result.lifecycleRan;
270
+ if (shouldStop && lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(result.newState);
177
271
  return {
178
- shouldStop: result.isFinal && result.lifecycleRan,
179
- stateChanged: result.lifecycleRan
272
+ shouldStop,
273
+ stateChanged: result.lifecycleRan,
274
+ result
180
275
  };
181
276
  });
277
+ const shutdown = (exitReason) => Effect.gen(function* () {
278
+ yield* Ref.set(stoppedRef, true);
279
+ if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
280
+ settlePostponed(postponed, actorId, forkEffect);
281
+ const remaining = yield* Queue.takeAll(eventQueue);
282
+ for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
283
+ else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
284
+ actorId,
285
+ eventTag: entry.event._tag
286
+ })));
287
+ else if (entry._tag === "call") {
288
+ const currentState = yield* SubscriptionRef.get(stateRef);
289
+ forkEffect(Deferred.succeed(entry.reply, {
290
+ newState: currentState,
291
+ previousState: currentState,
292
+ transitioned: false,
293
+ lifecycleRan: false,
294
+ isFinal: machine.finalStates.has(currentState._tag),
295
+ hasReply: false,
296
+ deferReply: false,
297
+ reply: void 0,
298
+ postponed: false
299
+ }));
300
+ }
301
+ yield* Scope.close(stateScopeRef.current, Exit.void);
302
+ yield* setExit(exitReason);
303
+ });
182
304
  while (true) {
183
305
  const queued = yield* Queue.take(eventQueue);
184
- const { shouldStop, stateChanged } = yield* processQueued(queued).pipe(Effect.catchAllCause((cause) => {
185
- if (queued._tag === "sendWait") Effect.runFork(Deferred.succeed(queued.done, void 0));
186
- else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
187
- return Effect.failCause(cause);
306
+ if (queued._tag === "drain") {
307
+ yield* shutdown(ActorExit.Stopped);
308
+ yield* Deferred.succeed(queued.done, void 0);
309
+ return;
310
+ }
311
+ const eventQueued = queued;
312
+ const processInner = processQueued(eventQueued);
313
+ const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
314
+ return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
315
+ }) : processInner).pipe(Effect.catchAllCause((cause) => {
316
+ if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
317
+ else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
318
+ else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
319
+ return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
188
320
  }));
189
321
  if (shouldStop) {
190
- yield* Ref.set(stoppedRef, true);
191
- settlePostponed(postponed, actorId);
192
- const remaining = yield* Queue.takeAll(eventQueue);
193
- for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
194
- else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
195
- actorId,
196
- eventTag: entry.event._tag
197
- })));
198
- yield* Scope.close(stateScopeRef.current, Exit.void);
199
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
322
+ const finalState = yield* SubscriptionRef.get(stateRef);
323
+ yield* shutdown(ActorExit.Final(finalState));
200
324
  return;
201
325
  }
202
326
  let drainTriggered = stateChanged;
@@ -206,16 +330,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
206
330
  for (const entry of drained) {
207
331
  const drain = yield* processQueued(entry);
208
332
  if (drain.shouldStop) {
209
- yield* Ref.set(stoppedRef, true);
210
- settlePostponed(postponed, actorId);
211
- const remaining2 = yield* Queue.takeAll(eventQueue);
212
- for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
213
- else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
214
- actorId,
215
- eventTag: r.event._tag
216
- })));
217
- yield* Scope.close(stateScopeRef.current, Exit.void);
218
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
333
+ const finalState = yield* SubscriptionRef.get(stateRef);
334
+ yield* shutdown(ActorExit.Final(finalState));
219
335
  return;
220
336
  }
221
337
  if (drain.stateChanged) drainTriggered = true;
@@ -224,12 +340,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
224
340
  }
225
341
  });
226
342
  /** Settle all pending Deferreds in the postpone buffer on shutdown. */
227
- const settlePostponed = (postponed, actorId) => {
228
- for (const entry of postponed) if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
343
+ const settlePostponed = (postponed, actorId, forkFn) => {
344
+ for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
229
345
  actorId,
230
346
  eventTag: entry.event._tag
231
347
  })));
232
- else if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
348
+ else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
233
349
  postponed.length = 0;
234
350
  };
235
351
  //#endregion
@@ -1,5 +1,5 @@
1
- import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
- import { BuiltMachine, Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
1
+ import { MachineContext, SlotsDef } from "../slot.js";
2
+ import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
3
  import { ActorSystem } from "../actor.js";
4
4
  import { Cause, Effect, Scope } from "effect";
5
5
 
@@ -21,7 +21,7 @@ interface TransitionExecutionResult<S> {
21
21
  *
22
22
  * Used by:
23
23
  * - executeTransition (actor event loop, testing)
24
- * - persistent-actor replay (restore, replayTo)
24
+ * - Machine.replay (event sourcing restore)
25
25
  *
26
26
  * @internal
27
27
  */
@@ -29,7 +29,7 @@ declare const runTransitionHandler: <S extends {
29
29
  readonly _tag: string;
30
30
  }, E extends {
31
31
  readonly _tag: string;
32
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
32
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, transition: Transition<S, E, SD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
33
33
  newState: S;
34
34
  hasReply: boolean;
35
35
  deferReply: boolean;
@@ -50,7 +50,7 @@ declare const executeTransition: <S extends {
50
50
  readonly _tag: string;
51
51
  }, E extends {
52
52
  readonly _tag: string;
53
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
53
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
54
54
  newState: S;
55
55
  transitioned: boolean;
56
56
  reenter: boolean;
@@ -68,6 +68,8 @@ interface ProcessEventHooks<S, E> {
68
68
  readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
69
69
  /** Called when a transition handler or spawn effect fails with a defect */
70
70
  readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
71
+ /** Called when a forked spawn fiber defects — signals the runtime to set exitDeferred */
72
+ readonly onSpawnDefect?: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
71
73
  }
72
74
  /**
73
75
  * Error info for inspection hooks.
@@ -109,7 +111,7 @@ declare const shouldPostpone: <S extends {
109
111
  readonly _tag: string;
110
112
  }, E extends {
111
113
  readonly _tag: string;
112
- }, R>(machine: Machine<S, E, R, any, any, any, any>, stateTag: string, eventTag: string) => boolean;
114
+ }, R>(machine: Machine<S, E, R, any, any, any>, stateTag: string, eventTag: string) => boolean;
113
115
  /**
114
116
  * Process a single event through the machine.
115
117
  *
@@ -126,7 +128,7 @@ declare const processEventCore: <S extends {
126
128
  readonly _tag: string;
127
129
  }, E extends {
128
130
  readonly _tag: string;
129
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
131
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
130
132
  current: Scope.CloseableScope;
131
133
  }, system: ActorSystem, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
132
134
  newState: S;
@@ -148,7 +150,7 @@ declare const runSpawnEffects: <S extends {
148
150
  readonly _tag: string;
149
151
  }, E extends {
150
152
  readonly _tag: string;
151
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
153
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined, onSpawnDefect?: ((cause: Cause.Cause<unknown>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
152
154
  /**
153
155
  * Resolve which transition should fire for a given state and event.
154
156
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -157,7 +159,7 @@ declare const resolveTransition: <S extends {
157
159
  readonly _tag: string;
158
160
  }, E extends {
159
161
  readonly _tag: string;
160
- }, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => (typeof machine.transitions)[number] | undefined;
162
+ }, R>(machine: Machine<S, E, R, any, any, any>, currentState: S, event: E) => (typeof machine.transitions)[number] | undefined;
161
163
  /**
162
164
  * Invalidate cached index for a machine (call after mutation).
163
165
  */
@@ -166,14 +168,13 @@ declare const invalidateIndex: (machine: object) => void;
166
168
  * Find all transitions matching a state/event pair.
167
169
  * Returns empty array if no matches.
168
170
  *
169
- * Accepts both `Machine` and `BuiltMachine`.
170
171
  * O(1) lookup after first access (index is lazily built).
171
172
  */
172
173
  declare const findTransitions: <S extends {
173
174
  readonly _tag: string;
174
175
  }, E extends {
175
176
  readonly _tag: string;
176
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, GD, EFD, R>>;
177
+ }, R, SD extends SlotsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, SD>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, SD, R>>;
177
178
  /**
178
179
  * Find all spawn effects for a state.
179
180
  * Returns empty array if no matches.
@@ -184,6 +185,6 @@ declare const findSpawnEffects: <S extends {
184
185
  readonly _tag: string;
185
186
  }, E extends {
186
187
  readonly _tag: string;
187
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, GD, EFD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, EFD, R>>;
188
+ }, R, SD extends SlotsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, SD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, SD, R>>;
188
189
  //#endregion
189
190
  export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -1,5 +1,4 @@
1
1
  import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
2
- import { BuiltMachine } from "../machine.js";
3
2
  import { Cause, Effect, Exit, Scope } from "effect";
4
3
  //#region src/internal/transition.ts
5
4
  /**
@@ -18,7 +17,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
18
17
  *
19
18
  * Used by:
20
19
  * - executeTransition (actor event loop, testing)
21
- * - persistent-actor replay (restore, replayTo)
20
+ * - Machine.replay (event sourcing restore)
22
21
  *
23
22
  * @internal
24
23
  */
@@ -30,12 +29,10 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
30
29
  self,
31
30
  system
32
31
  };
33
- const { guards, effects } = machine._slots;
34
32
  const handlerCtx = {
35
33
  state,
36
34
  event,
37
- guards,
38
- effects
35
+ slots: machine._slots
39
36
  };
40
37
  const raw = transition.handler(handlerCtx);
41
38
  const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
@@ -119,7 +116,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
119
116
  state: currentState,
120
117
  event,
121
118
  cause
122
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
119
+ }).pipe(Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
123
120
  }));
124
121
  if (!result.transitioned) return {
125
122
  newState: currentState,
@@ -139,7 +136,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
139
136
  stateScopeRef.current = yield* Scope.make();
140
137
  if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
141
138
  if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
142
- yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError);
139
+ yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
143
140
  }
144
141
  return {
145
142
  newState,
@@ -158,7 +155,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
158
155
  *
159
156
  * @internal
160
157
  */
161
- const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError) {
158
+ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
162
159
  const spawnEffects = findSpawnEffects(machine, state._tag);
163
160
  const ctx = {
164
161
  actorId,
@@ -167,25 +164,27 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
167
164
  self,
168
165
  system
169
166
  };
170
- const { effects: effectSlots } = machine._slots;
167
+ const slots = machine._slots;
171
168
  const reportError = onError;
169
+ const defectSignal = onSpawnDefect;
172
170
  for (const spawnEffect of spawnEffects) {
173
171
  const effect = spawnEffect.handler({
174
172
  actorId,
175
173
  state,
176
174
  event,
177
175
  self,
178
- effects: effectSlots,
176
+ slots,
179
177
  system
180
178
  }).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
181
179
  if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
182
- if (reportError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
183
- return reportError({
180
+ const report = reportError !== void 0 ? reportError({
184
181
  phase: "spawn",
185
182
  state,
186
183
  event,
187
184
  cause
188
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
185
+ }) : Effect.void;
186
+ const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
187
+ return report.pipe(Effect.andThen(signal), Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
189
188
  }));
190
189
  yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
191
190
  }
@@ -258,11 +257,10 @@ const getIndex = (machine) => {
258
257
  * Find all transitions matching a state/event pair.
259
258
  * Returns empty array if no matches.
260
259
  *
261
- * Accepts both `Machine` and `BuiltMachine`.
262
260
  * O(1) lookup after first access (index is lazily built).
263
261
  */
264
- const findTransitions = (input, stateTag, eventTag) => {
265
- const index = getIndex(input instanceof BuiltMachine ? input._inner : input);
262
+ const findTransitions = (machine, stateTag, eventTag) => {
263
+ const index = getIndex(machine);
266
264
  const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
267
265
  if (specific.length > 0) return specific;
268
266
  return index.transitions.get("*")?.get(eventTag) ?? [];
@@ -1,5 +1,9 @@
1
1
  import { Effect, Stream } from "effect";
2
2
  //#region src/internal/utils.ts
3
+ /**
4
+ * Internal utilities for effect-machine.
5
+ * @internal
6
+ */
3
7
  const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
4
8
  /**
5
9
  * Create a reply result for ask-bearing event handlers.
@@ -50,7 +54,7 @@ const getTag = (constructorOrValue) => {
50
54
  }
51
55
  };
52
56
  /** Check if a value is an Effect */
53
- const isEffect = (value) => typeof value === "object" && value !== null && Effect.EffectTypeId in value;
57
+ const isEffect = Effect.isEffect;
54
58
  /**
55
59
  * Stub ActorSystem that dies on any method call.
56
60
  * Used in contexts where spawning/system access isn't supported