effect-machine 0.11.0 → 0.12.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 (58) hide show
  1. package/dist/actor.d.ts +10 -4
  2. package/dist/actor.js +33 -5
  3. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  4. package/dist/cluster/adapters/in-memory.js +79 -0
  5. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  6. package/dist/cluster/entity-actor-ref.js +33 -0
  7. package/dist/cluster/entity-machine.d.ts +31 -49
  8. package/dist/cluster/entity-machine.js +167 -52
  9. package/dist/cluster/index.d.ts +5 -2
  10. package/dist/cluster/index.js +4 -1
  11. package/dist/cluster/persistence.d.ts +49 -0
  12. package/dist/cluster/persistence.js +18 -0
  13. package/dist/cluster/to-entity.d.ts +9 -3
  14. package/dist/cluster/to-entity.js +16 -4
  15. package/dist/errors.d.ts +12 -1
  16. package/dist/errors.js +8 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/internal/brands.d.ts +14 -1
  19. package/dist/internal/runtime.d.ts +67 -0
  20. package/dist/internal/runtime.js +248 -0
  21. package/dist/internal/transition.d.ts +5 -0
  22. package/dist/internal/transition.js +15 -3
  23. package/dist/internal/utils.d.ts +42 -6
  24. package/dist/internal/utils.js +27 -1
  25. package/dist/machine.d.ts +26 -13
  26. package/dist/machine.js +14 -3
  27. package/dist/schema.d.ts +35 -34
  28. package/dist/schema.js +32 -3
  29. package/dist/testing.js +4 -2
  30. package/package.json +3 -3
  31. package/v3/dist/actor.d.ts +4 -3
  32. package/v3/dist/actor.js +15 -3
  33. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  34. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  35. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  36. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  37. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  38. package/v3/dist/cluster/entity-machine.js +134 -50
  39. package/v3/dist/cluster/index.d.ts +5 -2
  40. package/v3/dist/cluster/index.js +4 -1
  41. package/v3/dist/cluster/persistence.d.ts +48 -0
  42. package/v3/dist/cluster/persistence.js +14 -0
  43. package/v3/dist/cluster/to-entity.d.ts +5 -2
  44. package/v3/dist/cluster/to-entity.js +12 -4
  45. package/v3/dist/errors.d.ts +16 -1
  46. package/v3/dist/errors.js +8 -1
  47. package/v3/dist/index.d.ts +3 -2
  48. package/v3/dist/internal/brands.d.ts +15 -1
  49. package/v3/dist/internal/runtime.d.ts +65 -0
  50. package/v3/dist/internal/runtime.js +236 -0
  51. package/v3/dist/internal/transition.d.ts +5 -0
  52. package/v3/dist/internal/transition.js +15 -3
  53. package/v3/dist/internal/utils.d.ts +42 -6
  54. package/v3/dist/internal/utils.js +27 -1
  55. package/v3/dist/machine.d.ts +19 -13
  56. package/v3/dist/machine.js +10 -2
  57. package/v3/dist/schema.d.ts +35 -34
  58. package/v3/dist/schema.js +29 -3
@@ -0,0 +1,236 @@
1
+ import { INTERNAL_INIT_EVENT } from "./utils.js";
2
+ import { NoReplyError } from "../errors.js";
3
+ import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
4
+ import { ActorSystem } from "../actor.js";
5
+ import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect";
6
+ //#region src/internal/runtime.ts
7
+ /**
8
+ * Shared runtime kernel for machine event processing.
9
+ *
10
+ * Provides a single-queue event loop with:
11
+ * - Sequential event processing (no split-mailbox race)
12
+ * - Postpone buffer with drain-on-state-change (gen_statem)
13
+ * - Background effect lifecycle
14
+ * - Spawn effect lifecycle (per-state scope)
15
+ * - Final state detection → stop
16
+ * - Reply settlement (call/ask Deferreds)
17
+ * - Reply schema validation
18
+ *
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
+ *
23
+ * @internal
24
+ */
25
+ /**
26
+ * Create a runtime for a machine. Returns a handle for sending events
27
+ * and querying state. The runtime owns:
28
+ * - Single event queue (all events serialized)
29
+ * - Event loop fiber
30
+ * - Postpone buffer
31
+ * - Background effects
32
+ * - State scope (spawn effects)
33
+ * - Final state detection
34
+ *
35
+ * @internal
36
+ */
37
+ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
38
+ const { actorId, hooks } = config;
39
+ const stateRef = yield* Ref.make(machine.initial);
40
+ const stoppedRef = yield* Ref.make(false);
41
+ const eventQueue = yield* config.queueFactory ?? Queue.unbounded();
42
+ const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
43
+ if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
44
+ _tag: "send",
45
+ event
46
+ });
47
+ });
48
+ const self = {
49
+ send: selfSend,
50
+ cast: selfSend,
51
+ spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
52
+ };
53
+ const stateScopeRef = { current: yield* Scope.make() };
54
+ const backgroundFibers = [];
55
+ const initEvent = { _tag: INTERNAL_INIT_EVENT };
56
+ const ctx = {
57
+ actorId,
58
+ state: machine.initial,
59
+ event: initEvent,
60
+ self,
61
+ system
62
+ };
63
+ const { effects: effectSlots } = machine._slots;
64
+ for (const bg of machine.backgroundEffects) {
65
+ const fiber = yield* Effect.forkDaemon(bg.handler({
66
+ actorId,
67
+ state: machine.initial,
68
+ event: initEvent,
69
+ self,
70
+ effects: effectSlots,
71
+ system
72
+ }).pipe(Effect.provideService(machine.Context, ctx)));
73
+ backgroundFibers.push(fiber);
74
+ }
75
+ yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError);
76
+ if (machine.finalStates.has(machine.initial._tag)) {
77
+ yield* Ref.set(stoppedRef, true);
78
+ yield* Scope.close(stateScopeRef.current, Exit.void);
79
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
80
+ return makeHandle(stateRef, stoppedRef, eventQueue, machine);
81
+ }
82
+ const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks));
83
+ const stop = Effect.gen(function* () {
84
+ if (yield* Ref.get(stoppedRef)) return;
85
+ yield* Ref.set(stoppedRef, true);
86
+ yield* Fiber.interrupt(loopFiber);
87
+ yield* Scope.close(stateScopeRef.current, Exit.void);
88
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
89
+ }).pipe(Effect.asVoid);
90
+ yield* Effect.addFinalizer(() => stop);
91
+ return {
92
+ ...makeHandle(stateRef, stoppedRef, eventQueue, machine),
93
+ stop
94
+ };
95
+ });
96
+ /**
97
+ * Build the runtime handle (send/ask/getState/isStopped).
98
+ * Shared between initial-final and normal paths.
99
+ */
100
+ const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
101
+ send: (event) => Effect.gen(function* () {
102
+ if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
103
+ _tag: "send",
104
+ event
105
+ });
106
+ }),
107
+ sendWait: (event) => Effect.gen(function* () {
108
+ if (!(yield* Ref.get(stoppedRef))) {
109
+ const done = yield* Deferred.make();
110
+ yield* Queue.offer(eventQueue, {
111
+ _tag: "sendWait",
112
+ event,
113
+ done
114
+ });
115
+ yield* Deferred.await(done);
116
+ }
117
+ }),
118
+ ask: (event) => Effect.gen(function* () {
119
+ if (yield* Ref.get(stoppedRef)) return yield* new NoReplyError({
120
+ actorId: "stopped",
121
+ eventTag: event._tag
122
+ });
123
+ const reply = yield* Deferred.make();
124
+ yield* Queue.offer(eventQueue, {
125
+ _tag: "ask",
126
+ event,
127
+ reply
128
+ });
129
+ return yield* Deferred.await(reply);
130
+ }),
131
+ getState: Ref.get(stateRef),
132
+ isStopped: Ref.get(stoppedRef),
133
+ stop: Effect.void
134
+ });
135
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks) {
136
+ const postponed = [];
137
+ const hasPostponeRules = machine.postponeRules.length > 0;
138
+ const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
139
+ const event = queued.event;
140
+ const currentState = yield* Ref.get(stateRef);
141
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
142
+ postponed.push({
143
+ _tag: "send",
144
+ event
145
+ });
146
+ if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
147
+ return {
148
+ shouldStop: false,
149
+ stateChanged: false
150
+ };
151
+ }
152
+ const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
153
+ if (result.transitioned) yield* Ref.set(stateRef, result.newState);
154
+ switch (queued._tag) {
155
+ case "sendWait":
156
+ yield* Deferred.succeed(queued.done, void 0);
157
+ break;
158
+ case "ask":
159
+ if (result.hasReply) {
160
+ const replySchema = machine._replySchemas?.get(event._tag);
161
+ if (replySchema !== void 0) {
162
+ let decoded;
163
+ try {
164
+ decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
165
+ } catch (decodeError) {
166
+ yield* Deferred.die(queued.reply, decodeError);
167
+ return yield* Effect.die(decodeError);
168
+ }
169
+ yield* Deferred.succeed(queued.reply, decoded);
170
+ } else yield* Deferred.succeed(queued.reply, result.reply);
171
+ } else yield* Deferred.fail(queued.reply, new NoReplyError({
172
+ actorId,
173
+ eventTag: event._tag
174
+ }));
175
+ break;
176
+ }
177
+ return {
178
+ shouldStop: result.isFinal && result.lifecycleRan,
179
+ stateChanged: result.lifecycleRan
180
+ };
181
+ });
182
+ while (true) {
183
+ const queued = yield* Queue.take(eventQueue);
184
+ const { shouldStop, stateChanged } = yield* processQueued(queued).pipe(Effect.catchAllCause((cause) => {
185
+ if (queued._tag === "sendWait") Effect.runFork(Deferred.succeed(queued.done, void 0));
186
+ else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
187
+ return Effect.failCause(cause);
188
+ }));
189
+ if (shouldStop) {
190
+ yield* Ref.set(stoppedRef, true);
191
+ settlePostponed(postponed, actorId);
192
+ const remaining = yield* Queue.takeAll(eventQueue);
193
+ for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
194
+ else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
195
+ actorId,
196
+ eventTag: entry.event._tag
197
+ })));
198
+ yield* Scope.close(stateScopeRef.current, Exit.void);
199
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
200
+ return;
201
+ }
202
+ let drainTriggered = stateChanged;
203
+ while (drainTriggered && postponed.length > 0) {
204
+ drainTriggered = false;
205
+ const drained = postponed.splice(0);
206
+ for (const entry of drained) {
207
+ const drain = yield* processQueued(entry);
208
+ if (drain.shouldStop) {
209
+ yield* Ref.set(stoppedRef, true);
210
+ settlePostponed(postponed, actorId);
211
+ const remaining2 = yield* Queue.takeAll(eventQueue);
212
+ for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
213
+ else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
214
+ actorId,
215
+ eventTag: r.event._tag
216
+ })));
217
+ yield* Scope.close(stateScopeRef.current, Exit.void);
218
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
219
+ return;
220
+ }
221
+ if (drain.stateChanged) drainTriggered = true;
222
+ }
223
+ }
224
+ }
225
+ });
226
+ /** Settle all pending Deferreds in the postpone buffer on shutdown. */
227
+ const settlePostponed = (postponed, actorId) => {
228
+ for (const entry of postponed) if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
229
+ actorId,
230
+ eventTag: entry.event._tag
231
+ })));
232
+ else if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
233
+ postponed.length = 0;
234
+ };
235
+ //#endregion
236
+ export { createRuntime };
@@ -32,6 +32,7 @@ declare const runTransitionHandler: <S extends {
32
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<{
33
33
  newState: S;
34
34
  hasReply: boolean;
35
+ deferReply: boolean;
35
36
  reply: unknown;
36
37
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
37
38
  /**
@@ -54,6 +55,7 @@ declare const executeTransition: <S extends {
54
55
  transitioned: boolean;
55
56
  reenter: boolean;
56
57
  hasReply: boolean;
58
+ deferReply: boolean;
57
59
  reply: unknown;
58
60
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
59
61
  /**
@@ -92,6 +94,8 @@ interface ProcessEventResult<S> {
92
94
  readonly isFinal: boolean;
93
95
  /** Whether the handler provided a reply (structural, not value-based) */
94
96
  readonly hasReply: boolean;
97
+ /** Whether the handler deferred the reply to a spawn handler (Machine.deferReply) */
98
+ readonly deferReply: boolean;
95
99
  /** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
96
100
  readonly reply?: unknown;
97
101
  /** Whether the event was postponed (buffered for retry after next state change) */
@@ -131,6 +135,7 @@ declare const processEventCore: <S extends {
131
135
  lifecycleRan: boolean;
132
136
  isFinal: boolean;
133
137
  hasReply: boolean;
138
+ deferReply: boolean;
134
139
  reply: unknown;
135
140
  postponed: boolean;
136
141
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
@@ -1,4 +1,4 @@
1
- import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
1
+ import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
2
2
  import { BuiltMachine } from "../machine.js";
3
3
  import { Cause, Effect, Exit, Scope } from "effect";
4
4
  //#region src/internal/transition.ts
@@ -39,14 +39,22 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
39
39
  };
40
40
  const raw = transition.handler(handlerCtx);
41
41
  const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
42
- if (resolved !== null && typeof resolved === "object" && "state" in resolved && "reply" in resolved && !("_tag" in resolved)) return {
42
+ if (isReplyResult(resolved)) return {
43
43
  newState: resolved.state,
44
44
  hasReply: true,
45
+ deferReply: false,
45
46
  reply: resolved.reply
46
47
  };
48
+ if (isDeferReplyResult(resolved)) return {
49
+ newState: resolved.state,
50
+ hasReply: false,
51
+ deferReply: true,
52
+ reply: void 0
53
+ };
47
54
  return {
48
55
  newState: resolved,
49
56
  hasReply: false,
57
+ deferReply: false,
50
58
  reply: void 0
51
59
  };
52
60
  });
@@ -68,14 +76,16 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
68
76
  transitioned: false,
69
77
  reenter: false,
70
78
  hasReply: false,
79
+ deferReply: false,
71
80
  reply: void 0
72
81
  };
73
- const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
82
+ const { newState, hasReply, deferReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
74
83
  return {
75
84
  newState,
76
85
  transitioned: true,
77
86
  reenter: transition.reenter === true,
78
87
  hasReply,
88
+ deferReply,
79
89
  reply
80
90
  };
81
91
  });
@@ -118,6 +128,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
118
128
  lifecycleRan: false,
119
129
  isFinal: false,
120
130
  hasReply: false,
131
+ deferReply: false,
121
132
  reply: void 0,
122
133
  postponed: false
123
134
  };
@@ -137,6 +148,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
137
148
  lifecycleRan: runLifecycle,
138
149
  isFinal: machine.finalStates.has(newState._tag),
139
150
  hasReply: result.hasReply,
151
+ deferReply: result.deferReply,
140
152
  reply: result.reply,
141
153
  postponed: false
142
154
  };
@@ -23,15 +23,51 @@ type InstanceOf<C> = C extends ((...args: unknown[]) => infer R) ? R : never;
23
23
  type TaggedConstructor<T extends {
24
24
  readonly _tag: string;
25
25
  }> = (args: Omit<T, "_tag">) => T;
26
- /** Reply tuple returned from transition handlers for ask support */
27
- interface TransitionReply<State> {
26
+ declare const ReplyResultSymbol: unique symbol;
27
+ type ReplyResultSymbol = typeof ReplyResultSymbol;
28
+ /**
29
+ * Branded reply result from a transition handler.
30
+ * Created via `Machine.reply(state, value)`.
31
+ */
32
+ interface ReplyResult<State, Reply> {
33
+ readonly state: State;
34
+ readonly reply: Reply;
35
+ readonly [ReplyResultSymbol]: true;
36
+ }
37
+ /**
38
+ * Create a reply result for ask-bearing event handlers.
39
+ */
40
+ declare const makeReply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
41
+ /**
42
+ * Type guard for ReplyResult (symbol-based, replaces duck-typing).
43
+ */
44
+ declare const isReplyResult: (value: unknown) => value is ReplyResult<unknown, unknown>;
45
+ declare const DeferReplySymbol: unique symbol;
46
+ type DeferReplySymbol = typeof DeferReplySymbol;
47
+ /**
48
+ * Branded deferred reply result from a transition handler.
49
+ * Signals that the reply will be settled later by `self.reply()` in a spawn handler.
50
+ * Created via `Machine.deferReply(state)`.
51
+ */
52
+ interface DeferReplyResult<State> {
28
53
  readonly state: State;
29
- readonly reply: unknown;
54
+ readonly [DeferReplySymbol]: true;
30
55
  }
31
56
  /**
32
- * Transition handler result - either a new state, reply tuple, or Effect producing one
57
+ * Create a deferred reply result. Handler returns this to signal
58
+ * "spawn handler will call self.reply(value) later".
59
+ */
60
+ declare const makeDeferReply: <State>(state: State) => DeferReplyResult<State>;
61
+ /**
62
+ * Type guard for DeferReplyResult.
63
+ */
64
+ declare const isDeferReplyResult: (value: unknown) => value is DeferReplyResult<unknown>;
65
+ /**
66
+ * Transition handler result.
67
+ * - When Reply is `never`: handler returns plain State (no reply allowed)
68
+ * - When Reply is concrete: handler must return ReplyResult via Machine.reply()
33
69
  */
34
- type TransitionResult<State, R> = State | TransitionReply<State> | Effect.Effect<State | TransitionReply<State>, never, R>;
70
+ type TransitionResult<State, R, Reply = never> = [Reply] extends [never] ? State | Effect.Effect<State, never, R> : ReplyResult<State, Reply> | DeferReplyResult<State> | Effect.Effect<ReplyResult<State, Reply> | DeferReplyResult<State>, never, R>;
35
71
  /**
36
72
  * Internal event tags used for lifecycle effect contexts.
37
73
  * Prefixed with $ to distinguish from user events.
@@ -62,4 +98,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
62
98
  */
63
99
  declare const stubSystem: ActorSystem;
64
100
  //#endregion
65
- export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionReply, TransitionResult, getTag, isEffect, stubSystem };
101
+ export { ArgsOf, DeferReplyResult, DeferReplySymbol, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, ReplyResult, ReplyResultSymbol, TagOf, TaggedConstructor, TransitionResult, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
@@ -1,5 +1,31 @@
1
1
  import { Effect, Stream } from "effect";
2
2
  //#region src/internal/utils.ts
3
+ const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
4
+ /**
5
+ * Create a reply result for ask-bearing event handlers.
6
+ */
7
+ const makeReply = (state, reply) => ({
8
+ state,
9
+ reply,
10
+ [ReplyResultSymbol]: true
11
+ });
12
+ /**
13
+ * Type guard for ReplyResult (symbol-based, replaces duck-typing).
14
+ */
15
+ const isReplyResult = (value) => value !== null && typeof value === "object" && ReplyResultSymbol in value;
16
+ const DeferReplySymbol = Symbol.for("effect-machine/DeferReply");
17
+ /**
18
+ * Create a deferred reply result. Handler returns this to signal
19
+ * "spawn handler will call self.reply(value) later".
20
+ */
21
+ const makeDeferReply = (state) => ({
22
+ state,
23
+ [DeferReplySymbol]: true
24
+ });
25
+ /**
26
+ * Type guard for DeferReplyResult.
27
+ */
28
+ const isDeferReplyResult = (value) => value !== null && typeof value === "object" && DeferReplySymbol in value;
3
29
  /**
4
30
  * Internal event tags used for lifecycle effect contexts.
5
31
  * Prefixed with $ to distinguish from user events.
@@ -42,4 +68,4 @@ const stubSystem = {
42
68
  subscribe: () => () => {}
43
69
  };
44
70
  //#endregion
45
- export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
71
+ export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
@@ -1,6 +1,6 @@
1
1
  import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
2
- import { TransitionResult } from "./internal/utils.js";
3
- import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
2
+ import { ReplyResult, TransitionResult } from "./internal/utils.js";
3
+ import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
4
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
5
  import { DuplicateActorError } from "./errors.js";
6
6
  import { findTransitions } from "./internal/transition.js";
@@ -9,7 +9,7 @@ import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
9
9
 
10
10
  //#region src/machine.d.ts
11
11
  declare namespace machine_d_exports {
12
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, spawn };
12
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, reply, spawn };
13
13
  }
14
14
  /**
15
15
  * Self reference for sending events back to the machine
@@ -45,9 +45,11 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
45
45
  readonly system: ActorSystem;
46
46
  }
47
47
  /**
48
- * Transition handler function
48
+ * Transition handler function.
49
+ * When Reply is concrete (event has a reply schema), handler must return Machine.reply().
50
+ * When Reply is never, handler returns plain state.
49
51
  */
50
- type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R>;
52
+ type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R, Reply = never> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R, Reply>;
51
53
  /**
52
54
  * State effect handler function
53
55
  */
@@ -171,6 +173,8 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
171
173
  };
172
174
  readonly stateSchema?: Schema.Schema<State, unknown, never>;
173
175
  readonly eventSchema?: Schema.Schema<Event, unknown, never>;
176
+ /** @internal */
177
+ readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
174
178
  /**
175
179
  * Context tag for accessing machine state/event/self in slot handlers.
176
180
  * Uses shared module-level tag for all machines.
@@ -186,24 +190,25 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
186
190
  }>;
187
191
  get guardsSchema(): GuardsSchema<GD> | undefined;
188
192
  get effectsSchema(): EffectsSchema<EFD> | undefined;
193
+ get replySchemas(): ReadonlyMap<string, Schema.Schema.Any>;
189
194
  /** @internal */
190
195
  constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
191
196
  from<NS extends VariantsUnion<_SD> & BrandedState, R1>(state: TaggedOrConstructor<NS>, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
192
197
  from<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, R1>(states: NS, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS[number] extends TaggedOrConstructor<infer S extends VariantsUnion<_SD> & BrandedState> ? S : never>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
193
198
  /** @internal */
194
- scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>, reenter: boolean): Machine<State, Event, R, _SD, _ED, GD, EFD>;
199
+ scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>, reenter: boolean): Machine<State, Event, R, _SD, _ED, GD, EFD>;
195
200
  /** Register transition for a single state */
196
- on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
201
+ on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
197
202
  /** Register transition for multiple states (handler receives union of state types) */
198
- on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
203
+ on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
199
204
  /**
200
205
  * Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
201
206
  * Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
202
207
  */
203
208
  /** Single state */
204
- reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
209
+ reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
205
210
  /** Multiple states */
206
- reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
211
+ reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
207
212
  /**
208
213
  * Register a wildcard transition that fires from any state when no specific transition matches.
209
214
  * Specific `.on()` transitions always take priority over `.onAny()`.
@@ -314,8 +319,8 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
314
319
  private readonly machine;
315
320
  private readonly states;
316
321
  constructor(machine: Machine<State, Event, R, _SD, _ED, GD, EFD>, states: ReadonlyArray<TaggedOrConstructor<SelectedState>>);
317
- on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
318
- reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
322
+ on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
323
+ reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
319
324
  }
320
325
  declare const make: typeof Machine.make;
321
326
  /**
@@ -346,5 +351,6 @@ declare const replay: <S extends {
346
351
  }, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
347
352
  from?: S;
348
353
  }) => Effect.Effect<S, never, R>;
354
+ declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
349
355
  //#endregion
350
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, spawn };
356
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, reply, spawn };
@@ -1,6 +1,6 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Inspector } from "./inspection.js";
3
- import { getTag, stubSystem } from "./internal/utils.js";
3
+ import { getTag, makeReply, stubSystem } from "./internal/utils.js";
4
4
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
5
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
6
  import { MachineContextTag } from "./slot.js";
@@ -14,6 +14,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
14
14
  findTransitions: () => findTransitions,
15
15
  make: () => make,
16
16
  replay: () => replay,
17
+ reply: () => reply,
17
18
  spawn: () => spawn
18
19
  });
19
20
  const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
@@ -69,6 +70,7 @@ var Machine = class Machine {
69
70
  /** @internal */ _slots;
70
71
  stateSchema;
71
72
  eventSchema;
73
+ /** @internal */ _replySchemas;
72
74
  /**
73
75
  * Context tag for accessing machine state/event/self in slot handlers.
74
76
  * Uses shared module-level tag for all machines.
@@ -95,6 +97,9 @@ var Machine = class Machine {
95
97
  get effectsSchema() {
96
98
  return this._effectsSchema;
97
99
  }
100
+ get replySchemas() {
101
+ return this._replySchemas;
102
+ }
98
103
  /** @internal */
99
104
  constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
100
105
  this.initial = initial;
@@ -109,6 +114,7 @@ var Machine = class Machine {
109
114
  this._effectHandlers = /* @__PURE__ */ new Map();
110
115
  this.stateSchema = stateSchema;
111
116
  this.eventSchema = eventSchema;
117
+ this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
112
118
  this._slots = {
113
119
  guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
114
120
  const handler = this._guardHandlers.get(name);
@@ -371,6 +377,7 @@ var Machine = class Machine {
371
377
  result._spawnEffects = [...this._spawnEffects];
372
378
  result._backgroundEffects = [...this._backgroundEffects];
373
379
  result._postponeRules = [...this._postponeRules];
380
+ result._replySchemas = this._replySchemas;
374
381
  const anyHandlers = handlers;
375
382
  if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
376
383
  if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
@@ -460,5 +467,6 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
460
467
  }
461
468
  return state;
462
469
  });
470
+ const reply = makeReply;
463
471
  //#endregion
464
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
472
+ export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, reply, spawn };