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, SubscriptionRef } from "effect";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
6
7
  //#region src/internal/runtime.ts
7
8
  /**
8
9
  * Shared runtime kernel for machine event processing.
@@ -10,35 +11,42 @@ import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionR
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* SubscriptionRef.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 services = yield* Effect.services();
44
+ const fork = Effect.runForkWith(services);
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();
42
50
  const deferredReplyRef = { current: void 0 };
43
51
  const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
44
52
  if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
@@ -46,15 +54,18 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
46
54
  event
47
55
  });
48
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;
49
60
  const self = {
50
61
  send: selfSend,
51
62
  cast: selfSend,
52
- spawn: (childId, childMachine) => system.spawn(`${actorId}/${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,
53
64
  reply: (value) => Effect.sync(() => {
54
65
  const deferred = deferredReplyRef.current;
55
66
  if (deferred !== void 0) {
56
67
  deferredReplyRef.current = void 0;
57
- Effect.runFork(Deferred.succeed(deferred, value));
68
+ fork(Deferred.succeed(deferred, value));
58
69
  return true;
59
70
  }
60
71
  return false;
@@ -70,36 +81,67 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
70
81
  self,
71
82
  system
72
83
  };
73
- const { effects: effectSlots } = machine._slots;
84
+ const slots = machine._slots;
74
85
  for (const bg of machine.backgroundEffects) {
75
- const fiber = yield* Effect.forkDetach(bg.handler({
86
+ const fiber = yield* bg.handler({
76
87
  actorId,
77
88
  state: machine.initial,
78
89
  event: initEvent,
79
90
  self,
80
- effects: effectSlots,
91
+ slots,
81
92
  system
82
- }).pipe(Effect.provideService(machine.Context, ctx)));
93
+ }).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
83
94
  backgroundFibers.push(fiber);
84
95
  }
85
- 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.catchCause((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);
86
110
  if (machine.finalStates.has(machine.initial._tag)) {
111
+ if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
87
112
  yield* Ref.set(stoppedRef, true);
88
113
  yield* Scope.close(stateScopeRef.current, Exit.void);
89
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
90
- 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);
91
117
  }
92
- const loopFiber = yield* Effect.forkDetach(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks, deferredReplyRef));
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.forkDetach(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.hasInterruptsOnly(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.forkDetach(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
+ }));
93
133
  const stop = Effect.gen(function* () {
94
134
  if (yield* Ref.get(stoppedRef)) return;
135
+ if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
95
136
  yield* Ref.set(stoppedRef, true);
96
137
  yield* Fiber.interrupt(loopFiber);
97
138
  yield* Scope.close(stateScopeRef.current, Exit.void);
98
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
139
+ yield* Scope.close(actorScope, Exit.void);
140
+ yield* setExit(ActorExit.Stopped);
99
141
  }).pipe(Effect.asVoid);
100
- yield* Effect.addFinalizer(() => stop);
142
+ if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
101
143
  return {
102
- ...makeHandle(stateRef, stoppedRef, eventQueue, machine),
144
+ ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
103
145
  stop
104
146
  };
105
147
  });
@@ -107,7 +149,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
107
149
  * Build the runtime handle (send/ask/getState/isStopped).
108
150
  * Shared between initial-final and normal paths.
109
151
  */
110
- const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
152
+ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
111
153
  send: (event) => Effect.gen(function* () {
112
154
  if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
113
155
  _tag: "send",
@@ -141,28 +183,65 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
141
183
  getState: SubscriptionRef.get(stateRef),
142
184
  stateRef,
143
185
  isStopped: Ref.get(stoppedRef),
144
- stop: Effect.void
186
+ stop: Effect.void,
187
+ _queue: eventQueue,
188
+ _stoppedRef: stoppedRef,
189
+ exitDeferred,
190
+ actorScope
145
191
  });
146
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks, deferredReplyRef) {
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);
147
196
  const postponed = [];
148
197
  const hasPostponeRules = machine.postponeRules.length > 0;
149
198
  const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
150
199
  const event = queued.event;
151
200
  const currentState = yield* SubscriptionRef.get(stateRef);
152
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);
153
217
  postponed.push({
154
218
  _tag: "send",
155
219
  event
156
220
  });
157
- if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
158
221
  return {
159
222
  shouldStop: false,
160
- 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
+ }
161
235
  };
162
236
  }
237
+ if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
163
238
  const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
164
239
  if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
240
+ if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
165
241
  switch (queued._tag) {
242
+ case "call":
243
+ yield* Deferred.succeed(queued.reply, result);
244
+ break;
166
245
  case "sendWait":
167
246
  yield* Deferred.succeed(queued.done, void 0);
168
247
  break;
@@ -186,29 +265,62 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
186
265
  }));
187
266
  break;
188
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);
189
271
  return {
190
- shouldStop: result.isFinal && result.lifecycleRan,
191
- stateChanged: result.lifecycleRan
272
+ shouldStop,
273
+ stateChanged: result.lifecycleRan,
274
+ result
192
275
  };
193
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.clear(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
+ });
194
304
  while (true) {
195
305
  const queued = yield* Queue.take(eventQueue);
196
- const { shouldStop, stateChanged } = yield* processQueued(queued).pipe(Effect.catchCause((cause) => {
197
- if (queued._tag === "sendWait") Effect.runFork(Deferred.failCause(queued.done, cause));
198
- else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
199
- 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.catchCause((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)));
200
320
  }));
201
321
  if (shouldStop) {
202
- yield* Ref.set(stoppedRef, true);
203
- settlePostponed(postponed, actorId);
204
- const remaining = yield* Queue.takeAll(eventQueue);
205
- for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
206
- else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
207
- actorId,
208
- eventTag: entry.event._tag
209
- })));
210
- yield* Scope.close(stateScopeRef.current, Exit.void);
211
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
322
+ const finalState = yield* SubscriptionRef.get(stateRef);
323
+ yield* shutdown(ActorExit.Final(finalState));
212
324
  return;
213
325
  }
214
326
  let drainTriggered = stateChanged;
@@ -218,16 +330,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
218
330
  for (const entry of drained) {
219
331
  const drain = yield* processQueued(entry);
220
332
  if (drain.shouldStop) {
221
- yield* Ref.set(stoppedRef, true);
222
- settlePostponed(postponed, actorId);
223
- const remaining2 = yield* Queue.takeAll(eventQueue);
224
- for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
225
- else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
226
- actorId,
227
- eventTag: r.event._tag
228
- })));
229
- yield* Scope.close(stateScopeRef.current, Exit.void);
230
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
333
+ const finalState = yield* SubscriptionRef.get(stateRef);
334
+ yield* shutdown(ActorExit.Final(finalState));
231
335
  return;
232
336
  }
233
337
  if (drain.stateChanged) drainTriggered = true;
@@ -236,12 +340,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
236
340
  }
237
341
  });
238
342
  /** Settle all pending Deferreds in the postpone buffer on shutdown. */
239
- const settlePostponed = (postponed, actorId) => {
240
- 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({
241
345
  actorId,
242
346
  eventTag: entry.event._tag
243
347
  })));
244
- 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));
245
349
  postponed.length = 0;
246
350
  };
247
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
 
@@ -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.Closeable;
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.Closeable, 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.Closeable, 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
  /**
@@ -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;
@@ -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.catchCause((cause) => {
181
179
  if (Cause.hasInterruptsOnly(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.andThen(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) ?? [];