effect-machine 0.13.0 → 0.15.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 (49) hide show
  1. package/README.md +19 -11
  2. package/dist/actor.d.ts +18 -6
  3. package/dist/actor.js +151 -61
  4. package/dist/cluster/entity-machine.d.ts +1 -1
  5. package/dist/cluster/entity-machine.js +2 -1
  6. package/dist/cluster/to-entity.d.ts +1 -1
  7. package/dist/errors.d.ts +9 -2
  8. package/dist/errors.js +8 -2
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.js +4 -4
  11. package/dist/internal/runtime.d.ts +21 -2
  12. package/dist/internal/runtime.js +64 -56
  13. package/dist/internal/transition.d.ts +9 -9
  14. package/dist/internal/transition.js +3 -5
  15. package/dist/machine.d.ts +129 -135
  16. package/dist/machine.js +97 -112
  17. package/dist/schema.d.ts +17 -1
  18. package/dist/schema.js +10 -0
  19. package/dist/slot.d.ts +112 -86
  20. package/dist/slot.js +92 -59
  21. package/dist/testing.d.ts +16 -16
  22. package/dist/testing.js +3 -3
  23. package/package.json +3 -3
  24. package/v3/dist/actor.d.ts +25 -12
  25. package/v3/dist/actor.js +174 -89
  26. package/v3/dist/cluster/entity-machine.d.ts +1 -1
  27. package/v3/dist/cluster/entity-machine.js +1 -0
  28. package/v3/dist/cluster/to-entity.d.ts +1 -1
  29. package/v3/dist/errors.d.ts +12 -3
  30. package/v3/dist/errors.js +10 -4
  31. package/v3/dist/index.d.ts +6 -6
  32. package/v3/dist/index.js +2 -2
  33. package/v3/dist/inspection.d.ts +3 -22
  34. package/v3/dist/inspection.js +1 -15
  35. package/v3/dist/internal/brands.d.ts +4 -8
  36. package/v3/dist/internal/inspection.js +1 -1
  37. package/v3/dist/internal/runtime.d.ts +27 -8
  38. package/v3/dist/internal/runtime.js +91 -61
  39. package/v3/dist/internal/transition.d.ts +10 -10
  40. package/v3/dist/internal/transition.js +8 -10
  41. package/v3/dist/internal/utils.js +5 -1
  42. package/v3/dist/machine.d.ts +160 -120
  43. package/v3/dist/machine.js +118 -115
  44. package/v3/dist/schema.d.ts +25 -11
  45. package/v3/dist/schema.js +18 -5
  46. package/v3/dist/slot.d.ts +112 -86
  47. package/v3/dist/slot.js +92 -59
  48. package/v3/dist/testing.d.ts +16 -16
  49. package/v3/dist/testing.js +7 -7
@@ -1,10 +1,8 @@
1
1
  import { Brand } from "effect";
2
2
 
3
3
  //#region src/internal/brands.d.ts
4
- declare const StateTypeId: unique symbol;
5
- declare const EventTypeId: unique symbol;
6
- type StateTypeId = typeof StateTypeId;
7
- type EventTypeId = typeof EventTypeId;
4
+ type StateTypeId = "effect-machine/StateTypeId";
5
+ type EventTypeId = "effect-machine/EventTypeId";
8
6
  interface StateBrand extends Brand.Brand<StateTypeId> {}
9
7
  interface EventBrand extends Brand.Brand<EventTypeId> {}
10
8
  type BrandedState = {
@@ -13,8 +11,7 @@ type BrandedState = {
13
11
  type BrandedEvent = {
14
12
  readonly _tag: string;
15
13
  } & EventBrand;
16
- declare const SchemaIdTypeId: unique symbol;
17
- type SchemaIdTypeId = typeof SchemaIdTypeId;
14
+ type SchemaIdTypeId = "effect-machine/SchemaIdTypeId";
18
15
  /**
19
16
  * Brand that captures the schema definition type D.
20
17
  * Two schemas with identical definition shapes will have compatible brands.
@@ -33,8 +30,7 @@ type FullEventBrand<D extends Record<string, unknown>> = EventBrand & SchemaIdBr
33
30
  * Brand that carries the reply type for an event variant.
34
31
  * Present only on events defined with Event.reply().
35
32
  */
36
- declare const ReplyTypeId: unique symbol;
37
- type ReplyTypeId = typeof ReplyTypeId;
33
+ type ReplyTypeId = "effect-machine/ReplyTypeId";
38
34
  interface ReplyTypeBrand<R> extends Brand.Brand<ReplyTypeId> {
39
35
  readonly _ReplyType: R;
40
36
  }
@@ -14,7 +14,7 @@ const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function
14
14
  return;
15
15
  }
16
16
  });
17
- if (result !== void 0 && Effect.isEffect(result)) yield* result.pipe(Effect.catchAllCause(() => Effect.void));
17
+ if (Effect.isEffect(result)) yield* result.pipe(Effect.catchAllCause(() => Effect.void));
18
18
  });
19
19
  //#endregion
20
20
  export { emitWithTimestamp };
@@ -1,6 +1,6 @@
1
- import { ActorExit } from "../supervision.js";
2
1
  import { NoReplyError } from "../errors.js";
3
- import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
+ import { MachineContext, SlotsDef } from "../slot.js";
3
+ import { ActorExit } from "../supervision.js";
4
4
  import { ProcessEventHooks, ProcessEventResult } from "./transition.js";
5
5
  import { Machine, MachineRef } from "../machine.js";
6
6
  import { ActorSystem } from "../actor.js";
@@ -14,7 +14,7 @@ type RuntimeQueuedEvent<E> = {
14
14
  } | {
15
15
  readonly _tag: "sendWait";
16
16
  readonly event: E;
17
- readonly done: Deferred.Deferred<void>;
17
+ readonly done: Deferred.Deferred<void, unknown>;
18
18
  } | {
19
19
  readonly _tag: "call";
20
20
  readonly event: E;
@@ -43,18 +43,24 @@ interface RuntimeCellResources<S, E> {
43
43
  interface RuntimeHandle<S, E> {
44
44
  /** Enqueue a fire-and-forget event */
45
45
  readonly send: (event: E) => Effect.Effect<void>;
46
- /** Enqueue event and wait for processing to complete (for RPC Send) */
47
- readonly sendWait: (event: E) => Effect.Effect<void>;
46
+ /** Enqueue event and wait for processing to complete (for RPC Send). Fails on defect. */
47
+ readonly sendWait: (event: E) => Effect.Effect<void, unknown>;
48
48
  /** Enqueue an ask event, returns the reply value */
49
49
  readonly ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
50
50
  /** Get current state */
51
51
  readonly getState: Effect.Effect<S>;
52
- /** SubscriptionRef for state observation */
52
+ /** SubscriptionRef for state observation (WatchState streaming) */
53
53
  readonly stateRef: SubscriptionRef.SubscriptionRef<S>;
54
54
  /** Whether the runtime has stopped (final state reached) */
55
55
  readonly isStopped: Effect.Effect<boolean>;
56
56
  /** Stop the runtime (interrupt event loop, clean up) */
57
57
  readonly stop: Effect.Effect<void>;
58
+ /**
59
+ * Start the runtime — fork event loop, background effects, spawn effects.
60
+ * Idempotent: first caller runs initialization, subsequent callers await completion.
61
+ * Events sent before start() are queued and processed when start() runs.
62
+ */
63
+ readonly start: Effect.Effect<void>;
58
64
  /** @internal — raw event queue for direct enqueue (actor.ts uses this for pendingReplies tracking) */
59
65
  readonly _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
60
66
  /** @internal — stopped ref for direct access */
@@ -123,7 +129,7 @@ interface ProcessQueuedResult<S> {
123
129
  * and querying state. The runtime owns:
124
130
  * - Event loop fiber
125
131
  * - Postpone buffer
126
- * - Background effects
132
+ * - Background effects (under actorScope)
127
133
  * - State scope (spawn effects)
128
134
  * - Final state detection
129
135
  * - Exit reason via exitDeferred
@@ -137,6 +143,19 @@ declare const createRuntime: <S extends {
137
143
  readonly _tag: string;
138
144
  }, E extends {
139
145
  readonly _tag: string;
140
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<RuntimeHandle<S, E>, never, Scope.Scope | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
146
+ }, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<{
147
+ stop: Effect.Effect<void, never, never>;
148
+ start: Effect.Effect<void, unknown, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
149
+ send: (event: E) => Effect.Effect<void>;
150
+ sendWait: (event: E) => Effect.Effect<void, unknown>;
151
+ ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
152
+ getState: Effect.Effect<S, never, never>;
153
+ stateRef: SubscriptionRef.SubscriptionRef<S>;
154
+ isStopped: Effect.Effect<boolean>;
155
+ _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
156
+ _stoppedRef: Ref.Ref<boolean>;
157
+ exitDeferred: Deferred.Deferred<ActorExit<S>, never>;
158
+ actorScope: Scope.CloseableScope;
159
+ }, never, Scope.Scope>;
141
160
  //#endregion
142
161
  export { ProcessQueuedResult, RuntimeCellResources, RuntimeConfig, RuntimeHandle, RuntimeLifecycleHooks, RuntimeQueuedEvent, createRuntime };
@@ -3,7 +3,7 @@ import { NoReplyError } from "../errors.js";
3
3
  import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
4
4
  import { ActorExit } from "../supervision.js";
5
5
  import { ActorSystem } from "../actor.js";
6
- import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Runtime, Schema, Scope, SubscriptionRef } from "effect";
7
7
  //#region src/internal/runtime.ts
8
8
  /**
9
9
  * Shared runtime kernel for machine event processing.
@@ -28,7 +28,7 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
28
28
  * and querying state. The runtime owns:
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
34
  * - Exit reason via exitDeferred
@@ -40,11 +40,14 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
40
40
  */
41
41
  const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
42
42
  const { actorId, hooks, lifecycle } = config;
43
+ const rt = yield* Effect.runtime();
44
+ const fork = Runtime.runFork(rt);
43
45
  const stateRef = config.cellResources?.stateRef ?? (yield* SubscriptionRef.make(machine.initial));
44
46
  const stoppedRef = config.cellResources?.stoppedRef ?? (yield* Ref.make(false));
45
47
  const eventQueue = config.cellResources?.eventQueue ?? (yield* config.queueFactory ?? Queue.unbounded());
46
48
  const exitDeferred = yield* Deferred.make();
47
49
  const actorScope = yield* Scope.make();
50
+ const deferredReplyRef = { current: void 0 };
48
51
  const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
49
52
  if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
50
53
  _tag: "send",
@@ -57,10 +60,18 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
57
60
  const self = {
58
61
  send: selfSend,
59
62
  cast: selfSend,
60
- spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn
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
+ })
61
73
  };
62
74
  const stateScopeRef = { current: yield* Scope.make() };
63
- const backgroundFibers = [];
64
75
  const initEvent = { _tag: INTERNAL_INIT_EVENT };
65
76
  const ctx = {
66
77
  actorId,
@@ -69,65 +80,81 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
69
80
  self,
70
81
  system
71
82
  };
72
- const { effects: effectSlots } = machine._slots;
73
- for (const bg of machine.backgroundEffects) {
74
- const fiber = yield* Effect.forkDaemon(bg.handler({
75
- actorId,
76
- state: machine.initial,
77
- event: initEvent,
78
- self,
79
- effects: effectSlots,
80
- system
81
- }).pipe(Effect.provideService(machine.Context, ctx)));
82
- backgroundFibers.push(fiber);
83
- }
84
- if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
83
+ const slots = machine._slots;
85
84
  const loopFiberRef = { current: void 0 };
86
- const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid);
87
- yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
88
- return Effect.gen(function* () {
85
+ /** Set the exit deferred exactly once. */
86
+ const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
87
+ const startDeferred = yield* Deferred.make();
88
+ const startedRef = yield* Ref.make(false);
89
+ const start = Effect.gen(function* () {
90
+ if (yield* Ref.getAndSet(startedRef, true)) {
91
+ yield* Deferred.await(startDeferred);
92
+ return;
93
+ }
94
+ const backgroundFibers = [];
95
+ for (const bg of machine.backgroundEffects) {
96
+ const fiber = yield* bg.handler({
97
+ actorId,
98
+ state: machine.initial,
99
+ event: initEvent,
100
+ self,
101
+ slots,
102
+ system
103
+ }).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
104
+ backgroundFibers.push(fiber);
105
+ }
106
+ 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);
108
+ yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
109
+ return Effect.gen(function* () {
110
+ yield* Ref.set(stoppedRef, true);
111
+ yield* Scope.close(stateScopeRef.current, Exit.void);
112
+ yield* Scope.close(actorScope, Exit.void);
113
+ yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
114
+ return yield* Effect.failCause(cause);
115
+ });
116
+ }));
117
+ if (machine.finalStates.has(machine.initial._tag)) {
118
+ if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
89
119
  yield* Ref.set(stoppedRef, true);
90
120
  yield* Scope.close(stateScopeRef.current, Exit.void);
91
121
  yield* Scope.close(actorScope, Exit.void);
92
- yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
93
- return yield* Effect.failCause(cause);
94
- });
95
- }));
96
- /** Set the exit deferred exactly once. */
97
- const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
98
- if (machine.finalStates.has(machine.initial._tag)) {
99
- if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
100
- yield* Ref.set(stoppedRef, true);
101
- yield* Scope.close(stateScopeRef.current, Exit.void);
102
- yield* Scope.close(actorScope, Exit.void);
103
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
104
- yield* setExit(ActorExit.Final(machine.initial));
105
- return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
106
- }
107
- const augmentedHooks = {
108
- ...hooks,
109
- onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid)
110
- };
111
- const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, lifecycle, config.wrapProcess));
112
- loopFiberRef.current = loopFiber;
113
- if (backgroundFibers.length > 0) yield* Effect.forkDaemon(Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
114
- if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Fiber.interrupt(loopFiber)));
115
- return Effect.never;
116
- })))).pipe(Effect.catchAllCause(() => Effect.void)));
122
+ yield* setExit(ActorExit.Final(machine.initial));
123
+ yield* Deferred.succeed(startDeferred, void 0);
124
+ return;
125
+ }
126
+ const augmentedHooks = {
127
+ ...hooks,
128
+ 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)
129
+ };
130
+ const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
131
+ loopFiberRef.current = loopFiber;
132
+ if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
133
+ 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)));
134
+ return Effect.never;
135
+ })))).pipe(Effect.forkIn(actorScope));
136
+ yield* Effect.forkDaemon(Effect.gen(function* () {
137
+ const loopExit = yield* Fiber.await(loopFiber);
138
+ if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
139
+ else yield* Scope.close(actorScope, loopExit);
140
+ }));
141
+ yield* Deferred.succeed(startDeferred, void 0);
142
+ }).pipe(Effect.catchAllCause((cause) => Deferred.failCause(startDeferred, cause).pipe(Effect.andThen(Effect.failCause(cause)))));
117
143
  const stop = Effect.gen(function* () {
118
144
  if (yield* Ref.get(stoppedRef)) return;
119
145
  if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
120
146
  yield* Ref.set(stoppedRef, true);
121
- yield* Fiber.interrupt(loopFiber);
147
+ const loopFiber = loopFiberRef.current;
148
+ if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
122
149
  yield* Scope.close(stateScopeRef.current, Exit.void);
123
150
  yield* Scope.close(actorScope, Exit.void);
124
- yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
125
151
  yield* setExit(ActorExit.Stopped);
126
152
  }).pipe(Effect.asVoid);
127
153
  if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
128
154
  return {
129
155
  ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
130
- stop
156
+ stop,
157
+ start
131
158
  };
132
159
  });
133
160
  /**
@@ -169,12 +196,14 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope)
169
196
  stateRef,
170
197
  isStopped: Ref.get(stoppedRef),
171
198
  stop: Effect.void,
199
+ start: Effect.void,
172
200
  _queue: eventQueue,
173
201
  _stoppedRef: stoppedRef,
174
202
  exitDeferred,
175
203
  actorScope
176
204
  });
177
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, lifecycle, wrapProcess) {
205
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, wrapProcess, fork) {
206
+ const forkEffect = fork ?? Effect.runFork;
178
207
  /** Set the exit deferred exactly once. */
179
208
  const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
180
209
  const postponed = [];
@@ -242,7 +271,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
242
271
  }
243
272
  yield* Deferred.succeed(queued.reply, decoded);
244
273
  } else yield* Deferred.succeed(queued.reply, result.reply);
245
- } else yield* Deferred.fail(queued.reply, new NoReplyError({
274
+ } else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
275
+ else yield* Deferred.fail(queued.reply, new NoReplyError({
246
276
  actorId,
247
277
  eventTag: event._tag
248
278
  }));
@@ -260,16 +290,16 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
260
290
  const shutdown = (exitReason) => Effect.gen(function* () {
261
291
  yield* Ref.set(stoppedRef, true);
262
292
  if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
263
- settlePostponed(postponed, actorId);
293
+ settlePostponed(postponed, actorId, forkEffect);
264
294
  const remaining = yield* Queue.takeAll(eventQueue);
265
- for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
266
- else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
295
+ for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
296
+ else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
267
297
  actorId,
268
298
  eventTag: entry.event._tag
269
299
  })));
270
300
  else if (entry._tag === "call") {
271
301
  const currentState = yield* SubscriptionRef.get(stateRef);
272
- Effect.runFork(Deferred.succeed(entry.reply, {
302
+ forkEffect(Deferred.succeed(entry.reply, {
273
303
  newState: currentState,
274
304
  previousState: currentState,
275
305
  transitioned: false,
@@ -296,10 +326,10 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
296
326
  const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
297
327
  return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
298
328
  }) : processInner).pipe(Effect.catchAllCause((cause) => {
299
- if (queued._tag === "sendWait") Effect.runFork(Deferred.succeed(queued.done, void 0));
300
- else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
301
- else if (queued._tag === "call") Effect.runFork(Deferred.failCause(queued.reply, cause));
302
- return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.zipRight(Effect.failCause(cause)));
329
+ if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
330
+ else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
331
+ else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
332
+ return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
303
333
  }));
304
334
  if (shouldStop) {
305
335
  const finalState = yield* SubscriptionRef.get(stateRef);
@@ -323,12 +353,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
323
353
  }
324
354
  });
325
355
  /** Settle all pending Deferreds in the postpone buffer on shutdown. */
326
- const settlePostponed = (postponed, actorId) => {
327
- for (const entry of postponed) if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
356
+ const settlePostponed = (postponed, actorId, forkFn) => {
357
+ for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
328
358
  actorId,
329
359
  eventTag: entry.event._tag
330
360
  })));
331
- else if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
361
+ else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
332
362
  postponed.length = 0;
333
363
  };
334
364
  //#endregion
@@ -1,4 +1,4 @@
1
- import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
1
+ import { MachineContext, SlotsDef } from "../slot.js";
2
2
  import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
3
  import { ActorSystem } from "../actor.js";
4
4
  import { Cause, Effect, Scope } from "effect";
@@ -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;
@@ -111,7 +111,7 @@ declare const shouldPostpone: <S extends {
111
111
  readonly _tag: string;
112
112
  }, E extends {
113
113
  readonly _tag: string;
114
- }, 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;
115
115
  /**
116
116
  * Process a single event through the machine.
117
117
  *
@@ -128,7 +128,7 @@ declare const processEventCore: <S extends {
128
128
  readonly _tag: string;
129
129
  }, E extends {
130
130
  readonly _tag: string;
131
- }, 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: {
132
132
  current: Scope.CloseableScope;
133
133
  }, system: ActorSystem, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
134
134
  newState: S;
@@ -150,7 +150,7 @@ declare const runSpawnEffects: <S extends {
150
150
  readonly _tag: string;
151
151
  }, E extends {
152
152
  readonly _tag: string;
153
- }, 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, onSpawnDefect?: ((cause: Cause.Cause<unknown>) => 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>>;
154
154
  /**
155
155
  * Resolve which transition should fire for a given state and event.
156
156
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -159,7 +159,7 @@ declare const resolveTransition: <S extends {
159
159
  readonly _tag: string;
160
160
  }, E extends {
161
161
  readonly _tag: string;
162
- }, 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;
163
163
  /**
164
164
  * Invalidate cached index for a machine (call after mutation).
165
165
  */
@@ -174,7 +174,7 @@ declare const findTransitions: <S extends {
174
174
  readonly _tag: string;
175
175
  }, E extends {
176
176
  readonly _tag: string;
177
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: Machine<S, E, R, any, any, GD, EFD>, 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>>;
178
178
  /**
179
179
  * Find all spawn effects for a state.
180
180
  * Returns empty array if no matches.
@@ -185,6 +185,6 @@ declare const findSpawnEffects: <S extends {
185
185
  readonly _tag: string;
186
186
  }, E extends {
187
187
  readonly _tag: string;
188
- }, 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>>;
189
189
  //#endregion
190
190
  export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -17,7 +17,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
17
17
  *
18
18
  * Used by:
19
19
  * - executeTransition (actor event loop, testing)
20
- * - persistent-actor replay (restore, replayTo)
20
+ * - Machine.replay (event sourcing restore)
21
21
  *
22
22
  * @internal
23
23
  */
@@ -29,12 +29,10 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
29
29
  self,
30
30
  system
31
31
  };
32
- const { guards, effects } = machine._slots;
33
32
  const handlerCtx = {
34
33
  state,
35
34
  event,
36
- guards,
37
- effects
35
+ slots: machine._slots
38
36
  };
39
37
  const raw = transition.handler(handlerCtx);
40
38
  const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
@@ -118,7 +116,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
118
116
  state: currentState,
119
117
  event,
120
118
  cause
121
- }).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
119
+ }).pipe(Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
122
120
  }));
123
121
  if (!result.transitioned) return {
124
122
  newState: currentState,
@@ -166,7 +164,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
166
164
  self,
167
165
  system
168
166
  };
169
- const { effects: effectSlots } = machine._slots;
167
+ const slots = machine._slots;
170
168
  const reportError = onError;
171
169
  const defectSignal = onSpawnDefect;
172
170
  for (const spawnEffect of spawnEffects) {
@@ -175,7 +173,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
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;
@@ -186,7 +184,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
186
184
  cause
187
185
  }) : Effect.void;
188
186
  const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
189
- return report.pipe(Effect.zipRight(signal), Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
187
+ return report.pipe(Effect.andThen(signal), Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
190
188
  }));
191
189
  yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
192
190
  }
@@ -261,8 +259,8 @@ const getIndex = (machine) => {
261
259
  *
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);
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