effect-machine 0.9.0 → 0.10.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 (62) hide show
  1. package/README.md +76 -16
  2. package/dist/actor.d.ts +55 -89
  3. package/dist/actor.js +135 -30
  4. package/dist/cluster/entity-machine.js +5 -3
  5. package/dist/errors.d.ts +12 -1
  6. package/dist/errors.js +8 -1
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +2 -2
  9. package/dist/internal/transition.d.ts +26 -2
  10. package/dist/internal/transition.js +37 -8
  11. package/dist/internal/utils.d.ts +7 -2
  12. package/dist/machine.d.ts +66 -3
  13. package/dist/machine.js +65 -0
  14. package/dist/persistence/persistent-actor.js +52 -16
  15. package/dist/testing.js +57 -3
  16. package/package.json +9 -8
  17. package/{dist-v3 → v3/dist}/actor.d.ts +65 -78
  18. package/{dist-v3 → v3/dist}/actor.js +173 -35
  19. package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
  20. package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
  21. package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
  22. package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
  23. package/v3/dist/errors.d.ts +76 -0
  24. package/{dist-v3 → v3/dist}/errors.js +9 -2
  25. package/v3/dist/index.d.ts +13 -0
  26. package/v3/dist/index.js +13 -0
  27. package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
  28. package/v3/dist/inspection.js +156 -0
  29. package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
  30. package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
  31. package/v3/dist/internal/inspection.js +20 -0
  32. package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
  33. package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
  34. package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
  35. package/{dist-v3 → v3/dist}/internal/utils.js +1 -1
  36. package/{dist-v3 → v3/dist}/machine.d.ts +86 -9
  37. package/{dist-v3 → v3/dist}/machine.js +128 -2
  38. package/{dist-v3 → v3/dist}/persistence/adapter.d.ts +18 -5
  39. package/{dist-v3 → v3/dist}/persistence/adapter.js +1 -1
  40. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.d.ts +1 -1
  41. package/{dist-v3 → v3/dist}/persistence/adapters/in-memory.js +1 -1
  42. package/{dist-v3 → v3/dist}/persistence/persistent-actor.d.ts +7 -6
  43. package/{dist-v3 → v3/dist}/persistence/persistent-actor.js +58 -19
  44. package/{dist-v3 → v3/dist}/persistence/persistent-machine.d.ts +1 -1
  45. package/{dist-v3 → v3/dist}/persistence/persistent-machine.js +1 -1
  46. package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
  47. package/{dist-v3 → v3/dist}/schema.js +5 -2
  48. package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
  49. package/{dist-v3 → v3/dist}/slot.js +1 -1
  50. package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
  51. package/{dist-v3 → v3/dist}/testing.js +60 -6
  52. package/dist-v3/errors.d.ts +0 -27
  53. package/dist-v3/index.d.ts +0 -12
  54. package/dist-v3/index.js +0 -13
  55. package/dist-v3/inspection.js +0 -48
  56. package/dist-v3/internal/inspection.js +0 -13
  57. /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
  58. /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
  59. /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
  60. /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
  61. /package/{dist-v3 → v3/dist}/persistence/index.d.ts +0 -0
  62. /package/{dist-v3 → v3/dist}/persistence/index.js +0 -0
@@ -0,0 +1,156 @@
1
+ import { Context, Effect, Option } from "effect";
2
+ //#region src/inspection.ts
3
+ /**
4
+ * Inspector service tag - optional service for machine introspection
5
+ * Uses `any` types to allow variance flexibility when providing the service
6
+ */
7
+ const Inspector = Context.GenericTag("@effect/machine/Inspector");
8
+ /**
9
+ * Create an inspector from a sync callback function.
10
+ *
11
+ * Type params accept either raw tagged types or Schema constructors:
12
+ * - `makeInspector(cb)` — defaults to `AnyInspectionEvent`
13
+ * - `makeInspector<MyState, MyEvent>(cb)` — explicit tagged types
14
+ * - `makeInspector<typeof MyState, typeof MyEvent>(cb)` — schema constructors (auto-extracts `.Type`)
15
+ */
16
+ const makeInspector = (onInspect) => ({ onInspect });
17
+ /**
18
+ * Create an inspector from an Effect-returning callback function.
19
+ */
20
+ const makeInspectorEffect = (onInspect) => ({ onInspect });
21
+ /**
22
+ * Run an inspector handler, handling both sync and Effect returns.
23
+ * @internal
24
+ */
25
+ const inspectionEffect = (inspector, event) => {
26
+ const result = inspector.onInspect(event);
27
+ return Effect.isEffect(result) ? result : Effect.void;
28
+ };
29
+ /**
30
+ * Combine multiple inspectors into one. All run concurrently per event.
31
+ * Individual inspector failures are swallowed.
32
+ */
33
+ const combineInspectors = (...inspectors) => ({ onInspect: (event) => Effect.forEach(inspectors, (inspector) => inspectionEffect(inspector, event).pipe(Effect.catchAllCause(() => Effect.void)), {
34
+ concurrency: "unbounded",
35
+ discard: true
36
+ }) });
37
+ const inspectionSpanName = (event) => {
38
+ switch (event.type) {
39
+ case "@machine.spawn": return `Machine.inspect ${event.initialState._tag}`;
40
+ case "@machine.event": return `Machine.inspect ${event.event._tag}`;
41
+ case "@machine.transition": return `Machine.inspect ${event.fromState._tag}->${event.toState._tag}`;
42
+ case "@machine.effect": return `Machine.inspect ${event.effectType}`;
43
+ case "@machine.task": return `Machine.inspect task:${event.phase}`;
44
+ case "@machine.error": return `Machine.inspect ${event.phase}`;
45
+ case "@machine.stop": return `Machine.inspect ${event.finalState._tag}`;
46
+ }
47
+ };
48
+ const inspectionTraceName = (event) => {
49
+ switch (event.type) {
50
+ case "@machine.spawn": return `machine.spawn ${event.initialState._tag}`;
51
+ case "@machine.event": return `machine.event ${event.event._tag}`;
52
+ case "@machine.transition": return `machine.transition ${event.fromState._tag}->${event.toState._tag}`;
53
+ case "@machine.effect": return `machine.effect ${event.effectType}`;
54
+ case "@machine.task": return `machine.task ${event.phase}${event.taskName === void 0 ? "" : ` ${event.taskName}`}`;
55
+ case "@machine.error": return `machine.error ${event.phase}`;
56
+ case "@machine.stop": return `machine.stop ${event.finalState._tag}`;
57
+ }
58
+ };
59
+ const inspectionAttributes = (event) => {
60
+ const shared = {
61
+ "machine.actor.id": event.actorId,
62
+ "machine.inspection.type": event.type
63
+ };
64
+ switch (event.type) {
65
+ case "@machine.spawn": return {
66
+ ...shared,
67
+ "machine.state.initial": event.initialState._tag
68
+ };
69
+ case "@machine.event": return {
70
+ ...shared,
71
+ "machine.state.current": event.state._tag,
72
+ "machine.event.tag": event.event._tag
73
+ };
74
+ case "@machine.transition": return {
75
+ ...shared,
76
+ "machine.state.from": event.fromState._tag,
77
+ "machine.state.to": event.toState._tag,
78
+ "machine.event.tag": event.event._tag
79
+ };
80
+ case "@machine.effect": return {
81
+ ...shared,
82
+ "machine.state.current": event.state._tag,
83
+ "machine.effect.kind": event.effectType
84
+ };
85
+ case "@machine.task": return {
86
+ ...shared,
87
+ "machine.state.current": event.state._tag,
88
+ "machine.task.phase": event.phase,
89
+ ...event.taskName === void 0 ? {} : { "machine.task.name": event.taskName }
90
+ };
91
+ case "@machine.error": return {
92
+ ...shared,
93
+ "machine.phase": event.phase,
94
+ "machine.state.current": event.state._tag
95
+ };
96
+ case "@machine.stop": return {
97
+ ...shared,
98
+ "machine.state.final": event.finalState._tag
99
+ };
100
+ }
101
+ };
102
+ /**
103
+ * Inspector that emits OpenTelemetry spans and events for each inspection event.
104
+ */
105
+ const tracingInspector = (options) => ({ onInspect: (event) => {
106
+ const spanName = typeof options?.spanName === "function" ? options.spanName(event) : options?.spanName;
107
+ const traceName = options?.eventName?.(event) ?? inspectionTraceName(event);
108
+ const attributes = {
109
+ ...inspectionAttributes(event),
110
+ ...options?.attributes?.(event) ?? {}
111
+ };
112
+ return Effect.gen(function* () {
113
+ const currentSpan = yield* Effect.option(Effect.currentSpan);
114
+ if (Option.isSome(currentSpan)) currentSpan.value.event(traceName, BigInt(event.timestamp) * 1000000n, {
115
+ actorId: event.actorId,
116
+ inspectionType: event.type
117
+ });
118
+ }).pipe(Effect.withSpan(spanName ?? inspectionSpanName(event), { attributes }));
119
+ } });
120
+ /**
121
+ * Console inspector that logs events in a readable format
122
+ */
123
+ const consoleInspector = () => makeInspector((event) => {
124
+ const prefix = `[${event.actorId}]`;
125
+ switch (event.type) {
126
+ case "@machine.spawn":
127
+ console.log(prefix, "spawned →", event.initialState._tag);
128
+ break;
129
+ case "@machine.event":
130
+ console.log(prefix, "received", event.event._tag, "in", event.state._tag);
131
+ break;
132
+ case "@machine.transition":
133
+ console.log(prefix, event.fromState._tag, "→", event.toState._tag);
134
+ break;
135
+ case "@machine.effect":
136
+ console.log(prefix, event.effectType, "effect in", event.state._tag);
137
+ break;
138
+ case "@machine.task":
139
+ console.log(prefix, "task", event.phase, event.taskName ?? "<unnamed>", "in", event.state._tag);
140
+ break;
141
+ case "@machine.error":
142
+ console.log(prefix, "error in", event.phase, event.state._tag, "-", event.error);
143
+ break;
144
+ case "@machine.stop":
145
+ console.log(prefix, "stopped in", event.finalState._tag);
146
+ break;
147
+ }
148
+ });
149
+ /**
150
+ * Collecting inspector that stores events in an array for testing
151
+ */
152
+ const collectingInspector = (events) => ({ onInspect: (event) => {
153
+ events.push(event);
154
+ } });
155
+ //#endregion
156
+ export { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector };
@@ -1,6 +1,6 @@
1
1
  import { Brand } from "effect";
2
2
 
3
- //#region src-v3/internal/brands.d.ts
3
+ //#region src/internal/brands.d.ts
4
4
  declare const StateTypeId: unique symbol;
5
5
  declare const EventTypeId: unique symbol;
6
6
  type StateTypeId = typeof StateTypeId;
@@ -1,7 +1,7 @@
1
1
  import { InspectionEvent, Inspector } from "../inspection.js";
2
2
  import { Effect } from "effect";
3
3
 
4
- //#region src-v3/internal/inspection.d.ts
4
+ //#region src/internal/inspection.d.ts
5
5
  /**
6
6
  * Emit an inspection event with timestamp from Clock.
7
7
  * @internal
@@ -0,0 +1,20 @@
1
+ import { Clock, Effect } from "effect";
2
+ //#region src/internal/inspection.ts
3
+ /**
4
+ * Emit an inspection event with timestamp from Clock.
5
+ * @internal
6
+ */
7
+ const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* (inspector, makeEvent) {
8
+ if (inspector === void 0) return;
9
+ const event = makeEvent(yield* Clock.currentTimeMillis);
10
+ const result = yield* Effect.sync(() => {
11
+ try {
12
+ return inspector.onInspect(event);
13
+ } catch {
14
+ return;
15
+ }
16
+ });
17
+ if (result !== void 0 && Effect.isEffect(result)) yield* result.pipe(Effect.catchAllCause(() => Effect.void));
18
+ });
19
+ //#endregion
20
+ export { emitWithTimestamp };
@@ -1,9 +1,9 @@
1
- import { EffectsDef, GuardsDef } from "../slot.js";
1
+ import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
2
  import { BuiltMachine, Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
3
  import { ActorSystem } from "../actor.js";
4
4
  import { Cause, Effect, Scope } from "effect";
5
5
 
6
- //#region src-v3/internal/transition.d.ts
6
+ //#region src/internal/transition.d.ts
7
7
  /**
8
8
  * Result of executing a transition.
9
9
  */
@@ -29,7 +29,11 @@ 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) => Effect.Effect<S, never, Exclude<R, unknown>>;
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
+ newState: S;
34
+ hasReply: boolean;
35
+ reply: unknown;
36
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
33
37
  /**
34
38
  * Execute a transition for a given state and event.
35
39
  * Handles transition resolution, handler invocation, and guard/effect slot creation.
@@ -45,11 +49,13 @@ declare const executeTransition: <S extends {
45
49
  readonly _tag: string;
46
50
  }, E extends {
47
51
  readonly _tag: string;
48
- }, 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) => Effect.Effect<{
52
+ }, 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<{
49
53
  newState: S;
50
54
  transitioned: boolean;
51
55
  reenter: boolean;
52
- }, never, Exclude<R, unknown>>;
56
+ hasReply: boolean;
57
+ reply: unknown;
58
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
53
59
  /**
54
60
  * Optional hooks for event processing inspection/tracing.
55
61
  */
@@ -84,7 +90,22 @@ interface ProcessEventResult<S> {
84
90
  readonly lifecycleRan: boolean;
85
91
  /** Whether new state is final */
86
92
  readonly isFinal: boolean;
93
+ /** Whether the handler provided a reply (structural, not value-based) */
94
+ readonly hasReply: boolean;
95
+ /** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
96
+ readonly reply?: unknown;
97
+ /** Whether the event was postponed (buffered for retry after next state change) */
98
+ readonly postponed: boolean;
87
99
  }
100
+ /**
101
+ * Check if an event should be postponed in the current state.
102
+ * @internal
103
+ */
104
+ declare const shouldPostpone: <S extends {
105
+ readonly _tag: string;
106
+ }, E extends {
107
+ readonly _tag: string;
108
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, stateTag: string, eventTag: string) => boolean;
88
109
  /**
89
110
  * Process a single event through the machine.
90
111
  *
@@ -103,13 +124,16 @@ declare const processEventCore: <S extends {
103
124
  readonly _tag: string;
104
125
  }, 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: {
105
126
  current: Scope.CloseableScope;
106
- }, system: ActorSystem, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
107
- newState: any;
127
+ }, system: ActorSystem, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
128
+ newState: S;
108
129
  previousState: S;
109
130
  transitioned: boolean;
110
- lifecycleRan: any;
131
+ lifecycleRan: boolean;
111
132
  isFinal: boolean;
112
- }, unknown, unknown>;
133
+ hasReply: boolean;
134
+ reply: unknown;
135
+ postponed: boolean;
136
+ }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
113
137
  /**
114
138
  * Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
115
139
  *
@@ -119,7 +143,7 @@ declare const runSpawnEffects: <S extends {
119
143
  readonly _tag: string;
120
144
  }, E extends {
121
145
  readonly _tag: string;
122
- }, 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, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, unknown>;
146
+ }, 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) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
123
147
  /**
124
148
  * Resolve which transition should fire for a given state and event.
125
149
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -157,4 +181,4 @@ declare const findSpawnEffects: <S extends {
157
181
  readonly _tag: string;
158
182
  }, 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>>;
159
183
  //#endregion
160
- export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
184
+ export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -1,7 +1,7 @@
1
1
  import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
2
2
  import { BuiltMachine } from "../machine.js";
3
3
  import { Cause, Effect, Exit, Scope } from "effect";
4
- //#region src-v3/internal/transition.ts
4
+ //#region src/internal/transition.ts
5
5
  /**
6
6
  * Transition execution and indexing.
7
7
  *
@@ -22,8 +22,9 @@ import { Cause, Effect, Exit, Scope } from "effect";
22
22
  *
23
23
  * @internal
24
24
  */
25
- const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* (machine, transition, state, event, self, system) {
25
+ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* (machine, transition, state, event, self, system, actorId) {
26
26
  const ctx = {
27
+ actorId,
27
28
  state,
28
29
  event,
29
30
  self,
@@ -36,8 +37,18 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
36
37
  guards,
37
38
  effects
38
39
  };
39
- const result = transition.handler(handlerCtx);
40
- return isEffect(result) ? yield* result.pipe(Effect.provideService(machine.Context, ctx)) : result;
40
+ const raw = transition.handler(handlerCtx);
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 {
43
+ newState: resolved.state,
44
+ hasReply: true,
45
+ reply: resolved.reply
46
+ };
47
+ return {
48
+ newState: resolved,
49
+ hasReply: false,
50
+ reply: void 0
51
+ };
41
52
  });
42
53
  /**
43
54
  * Execute a transition for a given state and event.
@@ -50,20 +61,33 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
50
61
  *
51
62
  * @internal
52
63
  */
53
- const executeTransition = Effect.fn("effect-machine.executeTransition")(function* (machine, currentState, event, self, system) {
64
+ const executeTransition = Effect.fn("effect-machine.executeTransition")(function* (machine, currentState, event, self, system, actorId) {
54
65
  const transition = resolveTransition(machine, currentState, event);
55
66
  if (transition === void 0) return {
56
67
  newState: currentState,
57
68
  transitioned: false,
58
- reenter: false
69
+ reenter: false,
70
+ hasReply: false,
71
+ reply: void 0
59
72
  };
73
+ const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
60
74
  return {
61
- newState: yield* runTransitionHandler(machine, transition, currentState, event, self, system),
75
+ newState,
62
76
  transitioned: true,
63
- reenter: transition.reenter === true
77
+ reenter: transition.reenter === true,
78
+ hasReply,
79
+ reply
64
80
  };
65
81
  });
66
82
  /**
83
+ * Check if an event should be postponed in the current state.
84
+ * @internal
85
+ */
86
+ const shouldPostpone = (machine, stateTag, eventTag) => {
87
+ for (const rule of machine.postponeRules) if (rule.stateTag === stateTag && rule.eventTag === eventTag) return true;
88
+ return false;
89
+ };
90
+ /**
67
91
  * Process a single event through the machine.
68
92
  *
69
93
  * Handles:
@@ -75,8 +99,8 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
75
99
  *
76
100
  * @internal
77
101
  */
78
- const processEventCore = Effect.fn("effect-machine.processEventCore")(function* (machine, currentState, event, self, stateScopeRef, system, hooks) {
79
- const result = yield* executeTransition(machine, currentState, event, self, system).pipe(Effect.catchAllCause((cause) => {
102
+ const processEventCore = Effect.fn("effect-machine.processEventCore")(function* (machine, currentState, event, self, stateScopeRef, system, actorId, hooks) {
103
+ const result = yield* executeTransition(machine, currentState, event, self, system, actorId).pipe(Effect.catchAllCause((cause) => {
80
104
  if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
81
105
  const onError = hooks?.onError;
82
106
  if (onError === void 0) return Effect.failCause(cause).pipe(Effect.orDie);
@@ -92,7 +116,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
92
116
  previousState: currentState,
93
117
  transitioned: false,
94
118
  lifecycleRan: false,
95
- isFinal: false
119
+ isFinal: false,
120
+ hasReply: false,
121
+ reply: void 0,
122
+ postponed: false
96
123
  };
97
124
  const newState = result.newState;
98
125
  const runLifecycle = newState._tag !== currentState._tag || result.reenter;
@@ -101,14 +128,17 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
101
128
  stateScopeRef.current = yield* Scope.make();
102
129
  if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
103
130
  if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
104
- yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, hooks?.onError);
131
+ yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError);
105
132
  }
106
133
  return {
107
134
  newState,
108
135
  previousState: currentState,
109
136
  transitioned: true,
110
137
  lifecycleRan: runLifecycle,
111
- isFinal: machine.finalStates.has(newState._tag)
138
+ isFinal: machine.finalStates.has(newState._tag),
139
+ hasReply: result.hasReply,
140
+ reply: result.reply,
141
+ postponed: false
112
142
  };
113
143
  });
114
144
  /**
@@ -116,9 +146,10 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
116
146
  *
117
147
  * @internal
118
148
  */
119
- const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, onError) {
149
+ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError) {
120
150
  const spawnEffects = findSpawnEffects(machine, state._tag);
121
151
  const ctx = {
152
+ actorId,
122
153
  state,
123
154
  event,
124
155
  self,
@@ -128,6 +159,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
128
159
  const reportError = onError;
129
160
  for (const spawnEffect of spawnEffects) {
130
161
  const effect = spawnEffect.handler({
162
+ actorId,
131
163
  state,
132
164
  event,
133
165
  self,
@@ -233,4 +265,4 @@ const findSpawnEffects = (machine, stateTag) => {
233
265
  return getIndex(machine).spawn.get(stateTag) ?? [];
234
266
  };
235
267
  //#endregion
236
- export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler };
268
+ export { executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
@@ -1,7 +1,7 @@
1
1
  import { ActorSystem } from "../actor.js";
2
2
  import { Effect } from "effect";
3
3
 
4
- //#region src-v3/internal/utils.d.ts
4
+ //#region src/internal/utils.d.ts
5
5
  /**
6
6
  * Extracts _tag from a tagged union member
7
7
  */
@@ -23,10 +23,15 @@ 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> {
28
+ readonly state: State;
29
+ readonly reply: unknown;
30
+ }
26
31
  /**
27
- * Transition handler result - either a new state or Effect producing one
32
+ * Transition handler result - either a new state, reply tuple, or Effect producing one
28
33
  */
29
- type TransitionResult<State, R> = State | Effect.Effect<State, never, R>;
34
+ type TransitionResult<State, R> = State | TransitionReply<State> | Effect.Effect<State | TransitionReply<State>, never, R>;
30
35
  /**
31
36
  * Internal event tags used for lifecycle effect contexts.
32
37
  * Prefixed with $ to distinguish from user events.
@@ -57,4 +62,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
57
62
  */
58
63
  declare const stubSystem: ActorSystem;
59
64
  //#endregion
60
- export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionResult, getTag, isEffect, stubSystem };
65
+ export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor, TransitionReply, TransitionResult, getTag, isEffect, stubSystem };
@@ -1,5 +1,5 @@
1
1
  import { Effect, Stream } from "effect";
2
- //#region src-v3/internal/utils.ts
2
+ //#region src/internal/utils.ts
3
3
  /**
4
4
  * Internal event tags used for lifecycle effect contexts.
5
5
  * Prefixed with $ to distinguish from user events.
@@ -1,22 +1,24 @@
1
+ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
1
2
  import { TransitionResult } from "./internal/utils.js";
2
3
  import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
3
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
4
5
  import { PersistenceConfig, PersistentMachine } from "./persistence/persistent-machine.js";
5
6
  import { DuplicateActorError } from "./errors.js";
6
- import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
7
7
  import { findTransitions } from "./internal/transition.js";
8
8
  import { ActorRef, ActorSystem } from "./actor.js";
9
- import { Cause, Context, Effect, Schedule, Schema, Scope } from "effect";
9
+ import { Cause, Context, Duration, Effect, Schedule, Schema, Scope } from "effect";
10
10
 
11
- //#region src-v3/machine.d.ts
11
+ //#region src/machine.d.ts
12
12
  declare namespace machine_d_exports {
13
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, PersistenceConfig, PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, Transition, TransitionHandler, findTransitions, make, spawn };
13
+ export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, PersistenceConfig, PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, spawn };
14
14
  }
15
15
  /**
16
16
  * Self reference for sending events back to the machine
17
17
  */
18
18
  interface MachineRef<Event> {
19
19
  readonly send: (event: Event) => Effect.Effect<void>;
20
+ /** Fire-and-forget alias for send (OTP gen_server:cast). */
21
+ readonly cast: (event: Event) => Effect.Effect<void>;
20
22
  readonly spawn: <S2 extends {
21
23
  readonly _tag: string;
22
24
  }, E2 extends {
@@ -36,6 +38,7 @@ interface HandlerContext<State, Event, GD extends GuardsDef, ED extends EffectsD
36
38
  * Handler context passed to state effect handlers (onEnter, spawn, background)
37
39
  */
38
40
  interface StateHandlerContext<State, Event, ED extends EffectsDef> {
41
+ readonly actorId: string;
39
42
  readonly state: State;
40
43
  readonly event: Event;
41
44
  readonly self: MachineRef<Event>;
@@ -80,6 +83,23 @@ interface PersistOptions {
80
83
  readonly journalEvents: boolean;
81
84
  readonly machineType?: string;
82
85
  }
86
+ interface TaskOptions<State, Event, ED extends EffectsDef, A, E1, ES, EF> {
87
+ readonly onSuccess: (value: A, ctx: StateHandlerContext<State, Event, ED>) => ES;
88
+ readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<State, Event, ED>) => EF;
89
+ readonly name?: string;
90
+ }
91
+ /**
92
+ * Configuration for `.timeout()` — gen_statem-style state timeouts.
93
+ *
94
+ * Entering the state starts a timer. Leaving cancels it.
95
+ * `.reenter()` restarts the timer with fresh state values.
96
+ */
97
+ interface TimeoutConfig<State, Event> {
98
+ /** Duration before firing. Static or derived from current state. */
99
+ readonly duration: Duration.DurationInput | ((state: State) => Duration.DurationInput);
100
+ /** Event to send when the timer fires. Static or derived from current state. */
101
+ readonly event: Event | ((state: State) => Event);
102
+ }
83
103
  type IsAny<T> = 0 extends 1 & T ? true : false;
84
104
  type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
85
105
  type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
@@ -146,6 +166,11 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
146
166
  /** @internal */
147
167
  readonly _finalStates: Set<string>;
148
168
  /** @internal */
169
+ readonly _postponeRules: Array<{
170
+ readonly stateTag: string;
171
+ readonly eventTag: string;
172
+ }>;
173
+ /** @internal */
149
174
  readonly _guardsSchema?: GuardsSchema<GD>;
150
175
  /** @internal */
151
176
  readonly _effectsSchema?: EffectsSchema<EFD>;
@@ -169,10 +194,18 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
169
194
  get spawnEffects(): ReadonlyArray<SpawnEffect<State, Event, EFD, R>>;
170
195
  get backgroundEffects(): ReadonlyArray<BackgroundEffect<State, Event, EFD, R>>;
171
196
  get finalStates(): ReadonlySet<string>;
197
+ get postponeRules(): ReadonlyArray<{
198
+ readonly stateTag: string;
199
+ readonly eventTag: string;
200
+ }>;
172
201
  get guardsSchema(): GuardsSchema<GD> | undefined;
173
202
  get effectsSchema(): EffectsSchema<EFD> | undefined;
174
203
  /** @internal */
175
204
  constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
205
+ 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
+ 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
+ /** @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>;
176
209
  /** Register transition for a single state */
177
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>;
178
211
  /** Register transition for multiple states (handler receives union of state types) */
@@ -219,10 +252,29 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
219
252
  * State-scoped task that runs on entry and sends success/failure events.
220
253
  * Interrupts do not emit failure events.
221
254
  */
222
- task<NS extends VariantsUnion<_SD> & BrandedState, A, E1, ES extends VariantsUnion<_ED> & BrandedEvent, EF extends VariantsUnion<_ED> & BrandedEvent>(state: TaggedOrConstructor<NS>, run: (ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => Effect.Effect<A, E1, Scope.Scope>, options: {
223
- readonly onSuccess: (value: A, ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => ES;
224
- readonly onFailure?: (cause: Cause.Cause<E1>, ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => EF;
225
- }): Machine<State, Event, R, _SD, _ED, GD, EFD>;
255
+ task<NS extends VariantsUnion<_SD> & BrandedState, A, E1, ES extends VariantsUnion<_ED> & BrandedEvent, EF extends VariantsUnion<_ED> & BrandedEvent>(state: TaggedOrConstructor<NS>, run: (ctx: StateHandlerContext<NS, VariantsUnion<_ED> & BrandedEvent, EFD>) => Effect.Effect<A, E1, Scope.Scope>, options: TaskOptions<NS, VariantsUnion<_ED> & BrandedEvent, EFD, A, E1, ES, EF>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
256
+ /**
257
+ * State timeout gen_statem's `state_timeout`.
258
+ *
259
+ * Entering the state starts a timer. Leaving cancels it (via state scope).
260
+ * `.reenter()` restarts the timer with fresh state values.
261
+ * Compiles to `.task()` internally — preserves `@machine.task` inspection events.
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * machine
266
+ * .timeout(State.Loading, {
267
+ * duration: Duration.seconds(30),
268
+ * event: Event.Timeout,
269
+ * })
270
+ * // Dynamic duration from state
271
+ * .timeout(State.Retrying, {
272
+ * duration: (state) => Duration.seconds(state.backoff),
273
+ * event: Event.GiveUp,
274
+ * })
275
+ * ```
276
+ */
277
+ timeout<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, config: TimeoutConfig<NS, VariantsUnion<_ED> & BrandedEvent>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
226
278
  /**
227
279
  * Machine-lifetime effect that is forked on actor spawn and runs until the actor stops.
228
280
  * Use effect slots defined via `Slot.Effects` for the actual work.
@@ -244,6 +296,24 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
244
296
  * ```
245
297
  */
246
298
  background(handler: StateEffectHandler<State, Event, EFD, Scope.Scope>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
299
+ /**
300
+ * Postpone events — gen_statem's event postpone.
301
+ *
302
+ * When a matching event arrives in the given state, it is buffered instead of
303
+ * processed. After the next state transition (tag change), all buffered events
304
+ * are drained through the loop in FIFO order.
305
+ *
306
+ * Reply-bearing events (from `call`/`ask`) in the postpone buffer are settled
307
+ * with `ActorStoppedError` on stop/interrupt/final-state.
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * machine
312
+ * .postpone(State.Connecting, Event.Data) // single event
313
+ * .postpone(State.Connecting, [Event.Data, Event.Cmd]) // multiple events
314
+ * ```
315
+ */
316
+ postpone<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, events: TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent> | ReadonlyArray<TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
247
317
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
248
318
  /**
249
319
  * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
@@ -260,6 +330,13 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
260
330
  }, R>;
261
331
  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>;
262
332
  }
333
+ 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
+ private readonly machine;
335
+ private readonly states;
336
+ 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>;
339
+ }
263
340
  declare const make: typeof Machine.make;
264
341
  declare const spawn: {
265
342
  <S extends {
@@ -274,4 +351,4 @@ declare const spawn: {
274
351
  }, R>(machine: BuiltMachine<S, E, R>, id: string): Effect.Effect<ActorRef<S, E>, never, R>;
275
352
  };
276
353
  //#endregion
277
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, PersistOptions, type PersistenceConfig, type PersistentMachine, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, Transition, TransitionHandler, findTransitions, machine_d_exports, make, spawn };
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 };