effect-machine 0.10.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 (81) hide show
  1. package/README.md +65 -62
  2. package/dist/actor.d.ts +35 -97
  3. package/dist/actor.js +69 -98
  4. package/dist/cluster/adapters/in-memory.d.ts +28 -0
  5. package/dist/cluster/adapters/in-memory.js +79 -0
  6. package/dist/cluster/entity-actor-ref.d.ts +56 -0
  7. package/dist/cluster/entity-actor-ref.js +33 -0
  8. package/dist/cluster/entity-machine.d.ts +31 -49
  9. package/dist/cluster/entity-machine.js +167 -52
  10. package/dist/cluster/index.d.ts +5 -2
  11. package/dist/cluster/index.js +4 -1
  12. package/dist/cluster/persistence.d.ts +49 -0
  13. package/dist/cluster/persistence.js +18 -0
  14. package/dist/cluster/to-entity.d.ts +9 -3
  15. package/dist/cluster/to-entity.js +16 -4
  16. package/dist/errors.d.ts +12 -1
  17. package/dist/errors.js +8 -1
  18. package/dist/index.d.ts +5 -8
  19. package/dist/index.js +1 -6
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +67 -0
  22. package/dist/internal/runtime.js +248 -0
  23. package/dist/internal/transition.d.ts +6 -1
  24. package/dist/internal/transition.js +16 -4
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +28 -6
  27. package/dist/machine.d.ts +55 -46
  28. package/dist/machine.js +74 -13
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +32 -3
  31. package/dist/testing.js +4 -2
  32. package/package.json +4 -4
  33. package/v3/dist/actor.d.ts +29 -96
  34. package/v3/dist/actor.js +52 -97
  35. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  36. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  37. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  38. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  39. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  40. package/v3/dist/cluster/entity-machine.js +134 -50
  41. package/v3/dist/cluster/index.d.ts +5 -2
  42. package/v3/dist/cluster/index.js +4 -1
  43. package/v3/dist/cluster/persistence.d.ts +48 -0
  44. package/v3/dist/cluster/persistence.js +14 -0
  45. package/v3/dist/cluster/to-entity.d.ts +5 -2
  46. package/v3/dist/cluster/to-entity.js +12 -4
  47. package/v3/dist/errors.d.ts +16 -1
  48. package/v3/dist/errors.js +8 -1
  49. package/v3/dist/index.d.ts +5 -8
  50. package/v3/dist/index.js +1 -6
  51. package/v3/dist/internal/brands.d.ts +15 -1
  52. package/v3/dist/internal/runtime.d.ts +65 -0
  53. package/v3/dist/internal/runtime.js +236 -0
  54. package/v3/dist/internal/transition.d.ts +5 -0
  55. package/v3/dist/internal/transition.js +15 -3
  56. package/v3/dist/internal/utils.d.ts +42 -6
  57. package/v3/dist/internal/utils.js +28 -6
  58. package/v3/dist/machine.d.ts +48 -46
  59. package/v3/dist/machine.js +71 -13
  60. package/v3/dist/schema.d.ts +35 -34
  61. package/v3/dist/schema.js +29 -3
  62. package/dist/persistence/adapter.d.ts +0 -135
  63. package/dist/persistence/adapter.js +0 -25
  64. package/dist/persistence/adapters/in-memory.d.ts +0 -32
  65. package/dist/persistence/adapters/in-memory.js +0 -174
  66. package/dist/persistence/index.d.ts +0 -5
  67. package/dist/persistence/index.js +0 -5
  68. package/dist/persistence/persistent-actor.d.ts +0 -50
  69. package/dist/persistence/persistent-actor.js +0 -404
  70. package/dist/persistence/persistent-machine.d.ts +0 -105
  71. package/dist/persistence/persistent-machine.js +0 -22
  72. package/v3/dist/persistence/adapter.d.ts +0 -138
  73. package/v3/dist/persistence/adapter.js +0 -25
  74. package/v3/dist/persistence/adapters/in-memory.d.ts +0 -32
  75. package/v3/dist/persistence/adapters/in-memory.js +0 -174
  76. package/v3/dist/persistence/index.d.ts +0 -5
  77. package/v3/dist/persistence/index.js +0 -5
  78. package/v3/dist/persistence/persistent-actor.d.ts +0 -50
  79. package/v3/dist/persistence/persistent-actor.js +0 -404
  80. package/v3/dist/persistence/persistent-machine.d.ts +0 -105
  81. package/v3/dist/persistence/persistent-machine.js +0 -22
@@ -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.
@@ -33,17 +59,13 @@ const isEffect = (value) => typeof value === "object" && value !== null && Effec
33
59
  */
34
60
  const stubSystem = {
35
61
  spawn: () => Effect.die("spawn not supported in stub system"),
36
- restore: () => Effect.die("restore not supported in stub system"),
37
62
  get: () => Effect.die("get not supported in stub system"),
38
63
  stop: () => Effect.die("stop not supported in stub system"),
39
64
  events: Stream.empty,
40
65
  get actors() {
41
66
  return /* @__PURE__ */ new Map();
42
67
  },
43
- subscribe: () => () => {},
44
- listPersisted: () => Effect.die("listPersisted not supported in stub system"),
45
- restoreMany: () => Effect.die("restoreMany not supported in stub system"),
46
- restoreAll: () => Effect.die("restoreAll not supported in stub system")
68
+ subscribe: () => () => {}
47
69
  };
48
70
  //#endregion
49
- 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,16 +1,15 @@
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
- import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
6
5
  import { DuplicateActorError } from "./errors.js";
7
6
  import { findTransitions } from "./internal/transition.js";
8
7
  import { ActorRef, ActorSystem } from "./actor.js";
9
- import { Cause, Context, Duration, Effect, Schedule, Schema, Scope } from "effect";
8
+ import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
10
9
 
11
10
  //#region src/machine.d.ts
12
11
  declare namespace machine_d_exports {
13
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, PersistenceConfig, PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, spawn };
12
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, reply, spawn };
14
13
  }
15
14
  /**
16
15
  * Self reference for sending events back to the machine
@@ -46,9 +45,11 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
46
45
  readonly system: ActorSystem;
47
46
  }
48
47
  /**
49
- * 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.
50
51
  */
51
- 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>;
52
53
  /**
53
54
  * State effect handler function
54
55
  */
@@ -75,14 +76,6 @@ interface SpawnEffect<State, Event, ED extends EffectsDef, R> {
75
76
  interface BackgroundEffect<State, Event, ED extends EffectsDef, R> {
76
77
  readonly handler: StateEffectHandler<State, Event, ED, R>;
77
78
  }
78
- /** Options for `persist` */
79
- interface PersistOptions {
80
- readonly snapshotSchedule: Schedule.Schedule<unknown, {
81
- readonly _tag: string;
82
- }>;
83
- readonly journalEvents: boolean;
84
- readonly machineType?: string;
85
- }
86
79
  interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
87
80
  readonly onSuccess: (value: A, ctx: StateHandlerContext<State, Event, ED>) => ES;
88
81
  readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
@@ -137,11 +130,6 @@ declare class BuiltMachine<State, Event, R = never> {
137
130
  /** @internal */
138
131
  constructor(machine: Machine<State, Event, R, any, any, any, any>);
139
132
  get initial(): State;
140
- persist(config: PersistOptions): PersistentMachine<State & {
141
- readonly _tag: string;
142
- }, Event & {
143
- readonly _tag: string;
144
- }, R>;
145
133
  }
146
134
  /**
147
135
  * Machine definition with fluent builder API.
@@ -185,6 +173,8 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
185
173
  };
186
174
  readonly stateSchema?: Schema.Schema<State, unknown, never>;
187
175
  readonly eventSchema?: Schema.Schema<Event, unknown, never>;
176
+ /** @internal */
177
+ readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
188
178
  /**
189
179
  * Context tag for accessing machine state/event/self in slot handlers.
190
180
  * Uses shared module-level tag for all machines.
@@ -200,24 +190,25 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
200
190
  }>;
201
191
  get guardsSchema(): GuardsSchema<GD> | undefined;
202
192
  get effectsSchema(): EffectsSchema<EFD> | undefined;
193
+ get replySchemas(): ReadonlyMap<string, Schema.Schema.Any>;
203
194
  /** @internal */
204
195
  constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
205
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>;
206
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>;
207
198
  /** @internal */
208
- 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>;
209
200
  /** Register transition for a single state */
210
- 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>;
211
202
  /** Register transition for multiple states (handler receives union of state types) */
212
- 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>;
213
204
  /**
214
205
  * Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
215
206
  * Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
216
207
  */
217
208
  /** Single state */
218
- 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>;
219
210
  /** Multiple states */
220
- 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>;
221
212
  /**
222
213
  * Register a wildcard transition that fires from any state when no specific transition matches.
223
214
  * Specific `.on()` transitions always take priority over `.onAny()`.
@@ -322,33 +313,44 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
322
313
  * - Machines without slots: call with no arguments.
323
314
  */
324
315
  build<R2 = never>(...args: HasSlots<GD, EFD> extends true ? [handlers: ProvideHandlers<State, Event, GD, EFD, R2>] : [handlers?: ProvideHandlers<State, Event, GD, EFD, R2>]): BuiltMachine<State, Event, R | NormalizeR<R2>>;
325
- /** @internal Persist from raw Machine — prefer BuiltMachine.persist() */
326
- persist(config: PersistOptions): PersistentMachine<State & {
327
- readonly _tag: string;
328
- }, Event & {
329
- readonly _tag: string;
330
- }, R>;
331
316
  static make<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD>;
332
317
  }
333
318
  declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema.Struct.Fields>, _ED extends Record<string, Schema.Struct.Fields>, GD extends GuardsDef, EFD extends EffectsDef, SelectedState extends VariantsUnion<_SD> & BrandedState> {
334
319
  private readonly machine;
335
320
  private readonly states;
336
321
  constructor(machine: Machine<State, Event, R, _SD, _ED, GD, EFD>, states: ReadonlyArray<TaggedOrConstructor<SelectedState>>);
337
- 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>;
338
- 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>;
339
324
  }
340
325
  declare const make: typeof Machine.make;
341
- declare const spawn: {
342
- <S extends {
343
- readonly _tag: string;
344
- }, E extends {
345
- readonly _tag: string;
346
- }, R>(machine: BuiltMachine<S, E, R>): Effect.Effect<ActorRef<S, E>, never, R>;
347
- <S extends {
348
- readonly _tag: string;
349
- }, E extends {
350
- readonly _tag: string;
351
- }, R>(machine: BuiltMachine<S, E, R>, id: string): Effect.Effect<ActorRef<S, E>, never, R>;
352
- };
326
+ /**
327
+ * Spawn an actor from a built machine.
328
+ *
329
+ * Options:
330
+ * - `id` — custom actor ID (default: random)
331
+ * - `hydrate` restore from a previously-saved state snapshot.
332
+ * The actor starts in the hydrated state and re-runs spawn effects
333
+ * for that state (timers, scoped resources, etc.). Transition history
334
+ * is not replayed — only the current state's entry effects run.
335
+ *
336
+ * Persistence is composed in userland by observing `actor.changes`
337
+ * and saving snapshots to your own storage.
338
+ */
339
+ declare const spawn: <S extends {
340
+ readonly _tag: string;
341
+ }, E extends {
342
+ readonly _tag: string;
343
+ }, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
344
+ id?: string;
345
+ hydrate?: S;
346
+ }) => Effect.Effect<ActorRef<S, E>, never, R>;
347
+ declare const replay: <S extends {
348
+ readonly _tag: string;
349
+ }, E extends {
350
+ readonly _tag: string;
351
+ }, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
352
+ from?: S;
353
+ }) => Effect.Effect<S, never, R>;
354
+ declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
353
355
  //#endregion
354
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, type PersistenceConfig, type PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, 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 };