effect-machine 0.17.1 → 0.18.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -54
  3. package/dist/actor.d.ts +8 -45
  4. package/dist/actor.js +157 -231
  5. package/dist/cluster/adapters/in-memory.d.ts +0 -1
  6. package/dist/cluster/adapters/in-memory.js +10 -5
  7. package/dist/cluster/entity-actor-ref.d.ts +2 -3
  8. package/dist/cluster/entity-actor-ref.js +14 -17
  9. package/dist/cluster/entity-machine.d.ts +2 -8
  10. package/dist/cluster/entity-machine.js +57 -34
  11. package/dist/cluster/index.js +1 -1
  12. package/dist/cluster/persistence.d.ts +0 -1
  13. package/dist/cluster/to-entity.d.ts +19 -21
  14. package/dist/cluster/to-entity.js +18 -19
  15. package/dist/errors.d.ts +11 -37
  16. package/dist/errors.js +12 -30
  17. package/dist/index.d.ts +4 -5
  18. package/dist/index.js +4 -5
  19. package/dist/inspection.d.ts +0 -1
  20. package/dist/inspection.js +24 -13
  21. package/dist/internal/brands.d.ts +0 -1
  22. package/dist/internal/event-advancement.d.ts +50 -0
  23. package/dist/internal/event-advancement.js +79 -0
  24. package/dist/internal/inspection.d.ts +5 -9
  25. package/dist/internal/inspection.js +31 -10
  26. package/dist/internal/machine-definition.d.ts +16 -0
  27. package/dist/internal/runtime.d.ts +1 -161
  28. package/dist/internal/runtime.js +191 -150
  29. package/dist/internal/transition.d.ts +4 -135
  30. package/dist/internal/transition.js +82 -157
  31. package/dist/internal/utils.d.ts +3 -40
  32. package/dist/internal/utils.js +2 -2
  33. package/dist/machine.d.ts +47 -137
  34. package/dist/machine.js +147 -215
  35. package/dist/schema.d.ts +42 -21
  36. package/dist/schema.js +28 -18
  37. package/dist/supervision.d.ts +1 -24
  38. package/dist/supervision.js +2 -3
  39. package/dist/testing.d.ts +15 -29
  40. package/dist/testing.js +76 -95
  41. package/package.json +19 -38
  42. package/dist/slot.d.ts +0 -159
  43. package/dist/slot.js +0 -165
  44. package/v3/dist/_virtual/_rolldown/runtime.js +0 -13
  45. package/v3/dist/actor.d.ts +0 -250
  46. package/v3/dist/actor.js +0 -577
  47. package/v3/dist/cluster/adapters/in-memory.d.ts +0 -15
  48. package/v3/dist/cluster/adapters/in-memory.js +0 -62
  49. package/v3/dist/cluster/entity-actor-ref.d.ts +0 -49
  50. package/v3/dist/cluster/entity-actor-ref.js +0 -19
  51. package/v3/dist/cluster/entity-machine.d.ts +0 -74
  52. package/v3/dist/cluster/entity-machine.js +0 -166
  53. package/v3/dist/cluster/index.d.ts +0 -6
  54. package/v3/dist/cluster/index.js +0 -6
  55. package/v3/dist/cluster/persistence.d.ts +0 -48
  56. package/v3/dist/cluster/persistence.js +0 -14
  57. package/v3/dist/cluster/to-entity.d.ts +0 -69
  58. package/v3/dist/cluster/to-entity.js +0 -59
  59. package/v3/dist/errors.d.ts +0 -95
  60. package/v3/dist/errors.js +0 -54
  61. package/v3/dist/index.d.ts +0 -11
  62. package/v3/dist/index.js +0 -9
  63. package/v3/dist/inspection.d.ts +0 -151
  64. package/v3/dist/inspection.js +0 -128
  65. package/v3/dist/internal/brands.d.ts +0 -50
  66. package/v3/dist/internal/inspection.d.ts +0 -11
  67. package/v3/dist/internal/inspection.js +0 -20
  68. package/v3/dist/internal/runtime.d.ts +0 -161
  69. package/v3/dist/internal/runtime.js +0 -360
  70. package/v3/dist/internal/transition.d.ts +0 -190
  71. package/v3/dist/internal/transition.js +0 -278
  72. package/v3/dist/internal/utils.d.ts +0 -101
  73. package/v3/dist/internal/utils.js +0 -75
  74. package/v3/dist/machine.d.ts +0 -398
  75. package/v3/dist/machine.js +0 -487
  76. package/v3/dist/schema.d.ts +0 -174
  77. package/v3/dist/schema.js +0 -206
  78. package/v3/dist/slot.d.ts +0 -158
  79. package/v3/dist/slot.js +0 -165
  80. package/v3/dist/supervision.d.ts +0 -97
  81. package/v3/dist/supervision.js +0 -42
  82. package/v3/dist/testing.d.ts +0 -151
  83. package/v3/dist/testing.js +0 -189
  84. /package/{v3/dist/internal/brands.js → dist/internal/machine-definition.js} +0 -0
@@ -1,6 +1,7 @@
1
- import { INTERNAL_INIT_EVENT } from "./utils.js";
2
- import { NoReplyError } from "../errors.js";
3
- import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
1
+ import { INTERNAL_INIT_EVENT, isEffect } from "./utils.js";
2
+ import { ActorStoppedError, NoReplyError } from "../errors.js";
3
+ import { makeEventAdvancement } from "./event-advancement.js";
4
+ import { processEventCoreImmediate, runSpawnEffects, shouldPostpone } from "./transition.js";
4
5
  import { ActorExit } from "../supervision.js";
5
6
  import { ActorSystem } from "../actor.js";
6
7
  import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
@@ -42,9 +43,8 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
42
43
  const { actorId, hooks, lifecycle } = config;
43
44
  const services = yield* Effect.context();
44
45
  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());
46
+ const { stateRef, stoppedRef, eventQueue } = config.cellResources;
47
+ const pendingRequests = /* @__PURE__ */ new Set();
48
48
  const exitDeferred = yield* Deferred.make();
49
49
  const actorScope = yield* Scope.make();
50
50
  const deferredReplyRef = { current: void 0 };
@@ -57,10 +57,11 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
57
57
  const childPrefix = config.childIdPrefix ?? "";
58
58
  const defaultSpawn = (childId, childMachine) => system.spawn(`${childPrefix}${childId}`, childMachine).pipe(Effect.provideService(ActorSystem, system));
59
59
  const onChildSpawned = config.onChildSpawned;
60
+ let spawn = defaultSpawn;
61
+ if (onChildSpawned !== void 0) spawn = (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child)));
60
62
  const self = {
61
63
  send: selfSend,
62
- cast: selfSend,
63
- spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn,
64
+ spawn,
64
65
  reply: (value) => Effect.sync(() => {
65
66
  const deferred = deferredReplyRef.current;
66
67
  if (deferred !== void 0) {
@@ -73,14 +74,6 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
73
74
  };
74
75
  const stateScopeRef = { current: yield* Scope.make() };
75
76
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
76
- const ctx = {
77
- actorId,
78
- state: machine.initial,
79
- event: initEvent,
80
- self,
81
- system
82
- };
83
- const slots = machine._slots;
84
77
  const loopFiberRef = { current: void 0 };
85
78
  /** Set the exit deferred exactly once. */
86
79
  const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
@@ -92,19 +85,22 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
92
85
  return;
93
86
  }
94
87
  const backgroundFibers = [];
95
- for (const bg of machine.backgroundEffects) {
88
+ for (const bg of machine._backgroundEffectEntries()) {
96
89
  const fiber = yield* bg.handler({
97
90
  actorId,
98
91
  state: machine.initial,
99
92
  event: initEvent,
100
93
  self,
101
- slots,
102
94
  system
103
- }).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
95
+ }).pipe(Effect.forkIn(actorScope));
104
96
  backgroundFibers.push(fiber);
105
97
  }
106
98
  if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
107
- 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
+ const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
100
+ const loopFiber = loopFiberRef.current;
101
+ if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
102
+ return Effect.void;
103
+ })), Effect.asVoid);
108
104
  yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchCause((cause) => Effect.gen(function* () {
109
105
  yield* Ref.set(stoppedRef, true);
110
106
  yield* Scope.close(stateScopeRef.current, Exit.void);
@@ -112,7 +108,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
112
108
  yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
113
109
  return yield* Effect.failCause(cause);
114
110
  })));
115
- if (machine.finalStates.has(machine.initial._tag)) {
111
+ if (machine._isFinal(machine.initial._tag)) {
116
112
  if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
117
113
  yield* Ref.set(stoppedRef, true);
118
114
  yield* Scope.close(stateScopeRef.current, Exit.void);
@@ -123,9 +119,13 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
123
119
  }
124
120
  const augmentedHooks = {
125
121
  ...hooks,
126
- 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)
122
+ onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
123
+ const loopFiber = loopFiberRef.current;
124
+ if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
125
+ return Effect.void;
126
+ })), Effect.asVoid)
127
127
  };
128
- const loopFiber = yield* Effect.forkDetach(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
128
+ const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
129
129
  loopFiberRef.current = loopFiber;
130
130
  if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
131
131
  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)));
@@ -141,6 +141,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
141
141
  const stop = Effect.gen(function* () {
142
142
  if (yield* Ref.get(stoppedRef)) return;
143
143
  if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
144
+ yield* settlePendingRequests(pendingRequests, actorId);
144
145
  yield* Ref.set(stoppedRef, true);
145
146
  const loopFiber = loopFiberRef.current;
146
147
  if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
@@ -150,105 +151,140 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
150
151
  }).pipe(Effect.asVoid);
151
152
  if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
152
153
  return {
153
- ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
154
- stop,
155
- start
154
+ ...makeHandle(actorId, stateRef, stoppedRef, eventQueue, pendingRequests, exitDeferred),
155
+ stop: stop.pipe(Effect.provide(services)),
156
+ start: start.pipe(Effect.provide(services))
156
157
  };
157
158
  });
158
159
  /**
159
- * Build the runtime handle (send/ask/getState/isStopped).
160
+ * Build the runtime handle.
160
161
  * Shared between initial-final and normal paths.
161
162
  */
162
- const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
163
- send: (event) => Effect.gen(function* () {
164
- if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
163
+ const makeHandle = (actorId, stateRef, stoppedRef, eventQueue, pendingRequests, exitDeferred) => {
164
+ const track = (deferred, settle) => {
165
+ pendingRequests.add(settle);
166
+ return Deferred.await(deferred).pipe(Effect.ensuring(Effect.sync(() => pendingRequests.delete(settle))));
167
+ };
168
+ const send = (event) => Ref.get(stoppedRef).pipe(Effect.flatMap((stopped) => {
169
+ if (stopped) return Effect.void;
170
+ return Queue.offer(eventQueue, {
165
171
  _tag: "send",
166
172
  event
167
- });
168
- }),
169
- sendWait: (event) => Effect.gen(function* () {
170
- if (!(yield* Ref.get(stoppedRef))) {
171
- const done = yield* Deferred.make();
173
+ }).pipe(Effect.asVoid);
174
+ }));
175
+ return {
176
+ send,
177
+ sendWait: (event) => Effect.gen(function* () {
178
+ if (!(yield* Ref.get(stoppedRef))) {
179
+ const done = yield* Deferred.make();
180
+ yield* Queue.offer(eventQueue, {
181
+ _tag: "sendWait",
182
+ event,
183
+ done
184
+ });
185
+ yield* track(done, (error) => Deferred.fail(done, error).pipe(Effect.asVoid));
186
+ }
187
+ }),
188
+ call: (event) => Effect.gen(function* () {
189
+ if (yield* Ref.get(stoppedRef)) return yield* ActorStoppedError.make({ actorId });
190
+ const reply = yield* Deferred.make();
191
+ yield* Queue.offer(eventQueue, {
192
+ _tag: "call",
193
+ event,
194
+ reply
195
+ });
196
+ return yield* track(reply, (error) => Deferred.fail(reply, error).pipe(Effect.asVoid));
197
+ }),
198
+ ask: (event) => Effect.gen(function* () {
199
+ if (yield* Ref.get(stoppedRef)) return yield* ActorStoppedError.make({ actorId });
200
+ const reply = yield* Deferred.make();
172
201
  yield* Queue.offer(eventQueue, {
173
- _tag: "sendWait",
202
+ _tag: "ask",
174
203
  event,
204
+ reply
205
+ });
206
+ return yield* track(reply, (error) => Deferred.fail(reply, error).pipe(Effect.asVoid));
207
+ }),
208
+ drain: Effect.gen(function* () {
209
+ if (yield* Ref.get(stoppedRef)) return;
210
+ const done = yield* Deferred.make();
211
+ yield* Queue.offer(eventQueue, {
212
+ _tag: "drain",
175
213
  done
176
214
  });
177
215
  yield* Deferred.await(done);
178
- }
179
- }),
180
- ask: (event) => Effect.gen(function* () {
181
- if (yield* Ref.get(stoppedRef)) return yield* new NoReplyError({
182
- actorId: "stopped",
183
- eventTag: event._tag
184
- });
185
- const reply = yield* Deferred.make();
186
- yield* Queue.offer(eventQueue, {
187
- _tag: "ask",
188
- event,
189
- reply
190
- });
191
- return yield* Deferred.await(reply);
192
- }),
193
- getState: SubscriptionRef.get(stateRef),
194
- stateRef,
195
- isStopped: Ref.get(stoppedRef),
196
- stop: Effect.void,
197
- start: Effect.void,
198
- _queue: eventQueue,
199
- _stoppedRef: stoppedRef,
200
- exitDeferred,
201
- actorScope
216
+ }).pipe(Effect.asVoid),
217
+ sendSync: (event) => {
218
+ if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
219
+ _tag: "send",
220
+ event
221
+ }));
222
+ },
223
+ getState: SubscriptionRef.get(stateRef),
224
+ stateRef,
225
+ stop: Effect.void,
226
+ start: Effect.void,
227
+ settlePendingRequests: settlePendingRequests(pendingRequests, actorId),
228
+ exitDeferred
229
+ };
230
+ };
231
+ const settlePendingRequests = (pendingRequests, actorId) => Effect.gen(function* () {
232
+ const error = ActorStoppedError.make({ actorId });
233
+ for (const settle of pendingRequests) yield* settle(error);
234
+ pendingRequests.clear();
202
235
  });
203
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, wrapProcess, fork) {
236
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
204
237
  const forkEffect = fork ?? Effect.runFork;
205
238
  /** Set the exit deferred exactly once. */
206
239
  const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
207
- const postponed = [];
208
- const hasPostponeRules = machine.postponeRules.length > 0;
209
- const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
240
+ const postponeQueued = Effect.fn("effect-machine.runtime.postponeQueued")(function* (currentState, queued) {
210
241
  const event = queued.event;
211
- const currentState = yield* SubscriptionRef.get(stateRef);
212
- if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
213
- if (queued._tag === "call") {
214
- const postponedResult = {
215
- newState: currentState,
216
- previousState: currentState,
217
- transitioned: false,
218
- lifecycleRan: false,
219
- isFinal: false,
220
- hasReply: false,
221
- deferReply: false,
222
- reply: void 0,
223
- postponed: true
224
- };
225
- yield* Deferred.succeed(queued.reply, postponedResult);
226
- }
227
- if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
228
- postponed.push({
242
+ const postponedResult = {
243
+ newState: currentState,
244
+ previousState: currentState,
245
+ transitioned: false,
246
+ lifecycleRan: false,
247
+ isFinal: false,
248
+ hasReply: false,
249
+ deferReply: false,
250
+ reply: void 0,
251
+ postponed: true
252
+ };
253
+ let input = queued;
254
+ if (queued._tag === "call") {
255
+ yield* Deferred.succeed(queued.reply, postponedResult);
256
+ input = {
257
+ _tag: "send",
258
+ event
259
+ };
260
+ } else if (queued._tag === "sendWait") {
261
+ yield* Deferred.succeed(queued.done, void 0);
262
+ input = {
229
263
  _tag: "send",
230
264
  event
231
- });
232
- return {
233
- shouldStop: false,
234
- stateChanged: false,
235
- result: {
236
- newState: currentState,
237
- previousState: currentState,
238
- transitioned: false,
239
- lifecycleRan: false,
240
- isFinal: false,
241
- hasReply: false,
242
- deferReply: false,
243
- reply: void 0,
244
- postponed: true
245
- }
246
265
  };
247
266
  }
267
+ return {
268
+ input,
269
+ value: {
270
+ shouldStop: false,
271
+ stateChanged: false,
272
+ result: postponedResult
273
+ }
274
+ };
275
+ });
276
+ const processQueued = (currentState, queued) => Effect.gen(function* () {
277
+ const event = queued.event;
248
278
  if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
249
- const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
279
+ const processing = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
280
+ let result;
281
+ if (isEffect(processing)) result = yield* processing;
282
+ else result = processing;
250
283
  if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
251
- if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
284
+ if (lifecycle?.onStateChange !== void 0 && result.transitioned) {
285
+ const stateChange = lifecycle.onStateChange(result, event);
286
+ if (isEffect(stateChange)) yield* stateChange;
287
+ }
252
288
  switch (queued._tag) {
253
289
  case "call":
254
290
  yield* Deferred.succeed(queued.reply, result);
@@ -256,24 +292,25 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
256
292
  case "sendWait":
257
293
  yield* Deferred.succeed(queued.done, void 0);
258
294
  break;
259
- case "ask":
260
- if (result.hasReply) {
261
- const replySchema = machine._replySchemas?.get(event._tag);
262
- if (replySchema !== void 0) {
263
- const decoded = yield* Schema.decodeUnknownEffect(replySchema)(result.reply).pipe(Effect.catch((decodeError) => Effect.gen(function* () {
264
- yield* Deferred.die(queued.reply, decodeError);
265
- return yield* Effect.die(decodeError);
266
- })));
267
- yield* Deferred.succeed(queued.reply, decoded);
268
- } else yield* Deferred.succeed(queued.reply, result.reply);
269
- } else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
270
- else yield* Deferred.fail(queued.reply, new NoReplyError({
271
- actorId,
272
- eventTag: event._tag
273
- }));
274
- break;
295
+ case "ask": if (result.hasReply) {
296
+ const replySchema = machine._replySchema(event._tag);
297
+ if (replySchema !== void 0) {
298
+ const decoded = yield* Schema.decodeUnknownEffect(replySchema)(result.reply).pipe(Effect.catch((decodeError) => Effect.gen(function* () {
299
+ yield* Deferred.die(queued.reply, decodeError);
300
+ return yield* Effect.die(decodeError);
301
+ })));
302
+ yield* Deferred.succeed(queued.reply, decoded);
303
+ } else yield* Deferred.succeed(queued.reply, result.reply);
304
+ } else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
305
+ else yield* Deferred.fail(queued.reply, NoReplyError.make({
306
+ actorId,
307
+ eventTag: event._tag
308
+ }));
309
+ }
310
+ if (lifecycle?.onProcessed !== void 0 && result.transitioned) {
311
+ const processed = lifecycle.onProcessed(result, event);
312
+ if (isEffect(processed)) yield* processed;
275
313
  }
276
- if (lifecycle?.onProcessed !== void 0 && result.transitioned) yield* lifecycle.onProcessed(result, event);
277
314
  const shouldStop = result.isFinal && result.lifecycleRan;
278
315
  if (shouldStop && lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(result.newState);
279
316
  return {
@@ -282,24 +319,48 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
282
319
  result
283
320
  };
284
321
  });
322
+ const initialState = yield* SubscriptionRef.get(stateRef);
323
+ let currentState = initialState;
324
+ const makePostponeAdvancement = () => makeEventAdvancement({
325
+ initial: initialState,
326
+ isFinal: (state) => machine._isFinal(state._tag),
327
+ shouldPostpone: (state, queued) => shouldPostpone(machine, state._tag, queued.event._tag),
328
+ postpone: postponeQueued,
329
+ process: (state, queued, _draining) => processQueued(state, queued).pipe(Effect.map((processed) => ({
330
+ state: processed.result.newState,
331
+ transitioned: processed.result.transitioned,
332
+ stateChanged: processed.stateChanged,
333
+ shouldStop: processed.shouldStop,
334
+ value: processed
335
+ }))),
336
+ discard: (queued) => {
337
+ if (queued._tag === "ask") return Deferred.fail(queued.reply, NoReplyError.make({
338
+ actorId,
339
+ eventTag: queued.event._tag
340
+ })).pipe(Effect.asVoid);
341
+ return Effect.void;
342
+ }
343
+ });
344
+ let advancement;
345
+ if (machine._hasPostponeRules()) advancement = makePostponeAdvancement();
285
346
  const shutdown = (exitReason) => Effect.gen(function* () {
286
347
  yield* Ref.set(stoppedRef, true);
287
348
  if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
288
- settlePostponed(postponed, actorId, forkEffect);
349
+ if (advancement !== void 0) yield* advancement.close();
289
350
  const remaining = yield* Queue.clear(eventQueue);
290
351
  for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
291
- else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
352
+ else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, NoReplyError.make({
292
353
  actorId,
293
354
  eventTag: entry.event._tag
294
355
  })));
295
356
  else if (entry._tag === "call") {
296
- const currentState = yield* SubscriptionRef.get(stateRef);
357
+ const state = yield* SubscriptionRef.get(stateRef);
297
358
  forkEffect(Deferred.succeed(entry.reply, {
298
- newState: currentState,
299
- previousState: currentState,
359
+ newState: state,
360
+ previousState: state,
300
361
  transitioned: false,
301
362
  lifecycleRan: false,
302
- isFinal: machine.finalStates.has(currentState._tag),
363
+ isFinal: machine._isFinal(state._tag),
303
364
  hasReply: false,
304
365
  deferReply: false,
305
366
  reply: void 0,
@@ -317,44 +378,24 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
317
378
  return;
318
379
  }
319
380
  const eventQueued = queued;
320
- const processInner = processQueued(eventQueued);
321
- const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
322
- return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
323
- }) : processInner).pipe(Effect.catchCause((cause) => {
381
+ const catchEventDefect = (effect) => effect.pipe(Effect.catchCause((cause) => {
324
382
  if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
325
383
  else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
326
384
  else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
327
385
  return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
328
386
  }));
329
- if (shouldStop) {
387
+ let stopped;
388
+ if (advancement === void 0) {
389
+ const processed = yield* catchEventDefect(processQueued(currentState, eventQueued));
390
+ currentState = processed.result.newState;
391
+ stopped = processed.shouldStop;
392
+ } else stopped = (yield* catchEventDefect(advancement.advance(eventQueued))).stopped;
393
+ if (stopped) {
330
394
  const finalState = yield* SubscriptionRef.get(stateRef);
331
395
  yield* shutdown(ActorExit.Final(finalState));
332
396
  return;
333
397
  }
334
- let drainTriggered = stateChanged;
335
- while (drainTriggered && postponed.length > 0) {
336
- drainTriggered = false;
337
- const drained = postponed.splice(0);
338
- for (const entry of drained) {
339
- const drain = yield* processQueued(entry);
340
- if (drain.shouldStop) {
341
- const finalState = yield* SubscriptionRef.get(stateRef);
342
- yield* shutdown(ActorExit.Final(finalState));
343
- return;
344
- }
345
- if (drain.stateChanged) drainTriggered = true;
346
- }
347
- }
348
398
  }
349
399
  });
350
- /** Settle all pending Deferreds in the postpone buffer on shutdown. */
351
- const settlePostponed = (postponed, actorId, forkFn) => {
352
- for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
353
- actorId,
354
- eventTag: entry.event._tag
355
- })));
356
- else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
357
- postponed.length = 0;
358
- };
359
400
  //#endregion
360
401
  export { createRuntime };
@@ -1,63 +1,6 @@
1
- import { MachineContextTag, SlotsDef } from "../slot.js";
2
- import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
- import { ActorSystemService } from "../actor.js";
4
- import { Cause, Effect, Scope } from "effect";
5
-
1
+ import { Machine } from "../machine.js";
2
+ import { Transition } from "./machine-definition.js";
6
3
  //#region src/internal/transition.d.ts
7
- /**
8
- * Result of executing a transition.
9
- */
10
- interface TransitionExecutionResult<S> {
11
- /** New state after transition (or current state if no transition matched) */
12
- readonly newState: S;
13
- /** Whether a transition was executed */
14
- readonly transitioned: boolean;
15
- /** Whether reenter was specified on the transition */
16
- readonly reenter: boolean;
17
- }
18
- /**
19
- * Run a transition handler and return the new state.
20
- * Shared logic for executing handlers with proper context.
21
- *
22
- * Used by:
23
- * - executeTransition (actor event loop, testing)
24
- * - Machine.replay (event sourcing restore)
25
- *
26
- * @internal
27
- */
28
- declare const runTransitionHandler: <S extends {
29
- readonly _tag: string;
30
- }, E extends {
31
- readonly _tag: string;
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: ActorSystemService, actorId: string) => Effect.Effect<{
33
- newState: S;
34
- hasReply: boolean;
35
- deferReply: boolean;
36
- reply: unknown;
37
- }, never, Exclude<R, MachineContextTag>>;
38
- /**
39
- * Execute a transition for a given state and event.
40
- * Handles transition resolution, handler invocation, and guard/effect slot creation.
41
- *
42
- * Used by:
43
- * - processEvent in actor.ts (actual actor event loop)
44
- * - simulate in testing.ts (pure transition simulation)
45
- * - createTestHarness.send in testing.ts (step-by-step testing)
46
- *
47
- * @internal
48
- */
49
- declare const executeTransition: <S extends {
50
- readonly _tag: string;
51
- }, E extends {
52
- readonly _tag: string;
53
- }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystemService, actorId: string) => Effect.Effect<{
54
- newState: S;
55
- transitioned: boolean;
56
- reenter: boolean;
57
- hasReply: boolean;
58
- deferReply: boolean;
59
- reply: unknown;
60
- }, never, Exclude<R, MachineContextTag>>;
61
4
  /**
62
5
  * Optional hooks for event processing inspection/tracing.
63
6
  */
@@ -103,54 +46,6 @@ interface ProcessEventResult<S> {
103
46
  /** Whether the event was postponed (buffered for retry after next state change) */
104
47
  readonly postponed: boolean;
105
48
  }
106
- /**
107
- * Check if an event should be postponed in the current state.
108
- * @internal
109
- */
110
- declare const shouldPostpone: <S extends {
111
- readonly _tag: string;
112
- }, E extends {
113
- readonly _tag: string;
114
- }, R>(machine: Machine<S, E, R, any, any, any>, stateTag: string, eventTag: string) => boolean;
115
- /**
116
- * Process a single event through the machine.
117
- *
118
- * Handles:
119
- * - Transition execution
120
- * - State scope lifecycle (close old, create new)
121
- * - Running spawn effects
122
- *
123
- * Optional hooks allow inspection/tracing without coupling to specific impl.
124
- *
125
- * @internal
126
- */
127
- declare const processEventCore: <S extends {
128
- readonly _tag: string;
129
- }, E extends {
130
- readonly _tag: string;
131
- }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
132
- current: Scope.Closeable;
133
- }, system: ActorSystemService, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
134
- newState: S;
135
- previousState: S;
136
- transitioned: boolean;
137
- lifecycleRan: boolean;
138
- isFinal: boolean;
139
- hasReply: boolean;
140
- deferReply: boolean;
141
- reply: unknown;
142
- postponed: boolean;
143
- }, never, Exclude<R, MachineContextTag> | Exclude<Exclude<R, MachineContextTag>, Scope.Scope>>;
144
- /**
145
- * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
146
- *
147
- * @internal
148
- */
149
- declare const runSpawnEffects: <S extends {
150
- readonly _tag: string;
151
- }, E extends {
152
- readonly _tag: string;
153
- }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.Closeable, system: ActorSystemService, 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, MachineContextTag>, Scope.Scope>>;
154
49
  /**
155
50
  * Resolve which transition should fire for a given state and event.
156
51
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -159,32 +54,6 @@ declare const resolveTransition: <S extends {
159
54
  readonly _tag: string;
160
55
  }, E extends {
161
56
  readonly _tag: string;
162
- }, R>(machine: Machine<S, E, R, any, any, any>, currentState: S, event: E) => (typeof machine.transitions)[number] | undefined;
163
- /**
164
- * Invalidate cached index for a machine (call after mutation).
165
- */
166
- declare const invalidateIndex: (machine: object) => void;
167
- /**
168
- * Find all transitions matching a state/event pair.
169
- * Returns empty array if no matches.
170
- *
171
- * O(1) lookup after first access (index is lazily built).
172
- */
173
- declare const findTransitions: <S extends {
174
- readonly _tag: string;
175
- }, E extends {
176
- readonly _tag: string;
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>>;
178
- /**
179
- * Find all spawn effects for a state.
180
- * Returns empty array if no matches.
181
- *
182
- * O(1) lookup after first access (index is lazily built).
183
- */
184
- declare const findSpawnEffects: <S extends {
185
- readonly _tag: string;
186
- }, E extends {
187
- readonly _tag: string;
188
- }, R, SD extends SlotsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, SD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, SD, R>>;
57
+ }, R>(machine: Machine<S, E, R, any, any>, currentState: S, event: E) => Transition<S, E, never> | undefined;
189
58
  //#endregion
190
- export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
59
+ export { ProcessEventError, ProcessEventHooks, ProcessEventResult, resolveTransition };