effect-machine 0.19.0 → 0.20.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.
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ import { DeferReplyResult, ReplyResult } from "./internal/utils.js";
2
2
  import { Event, MachineEventSchema, MachineStateSchema, ReplyFields, State } from "./schema.js";
3
3
  import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, NoReplyError, PersistenceError, VersionConflictError } from "./errors.js";
4
4
  import { ActorExit, DefectPhase, Supervision } from "./supervision.js";
5
- import { Durability, DurabilityCommit, HandlerContext, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, StateHandlerContext, TaskOptions, TimeoutConfig, machine_d_exports } from "./machine.js";
5
+ import { Durability, DurabilityCommit, FinalContext, GuardPredicate, HandlerContext, InputMakeConfig, Lifecycle, Machine, MachineRef, MakeConfig, Recovery, RecoveryContext, ReplayOptions, SpawnOptions, StateHandlerContext, TaskOptions, TimeoutConfig, machine_d_exports } from "./machine.js";
6
6
  import { ProcessEventResult } from "./internal/transition.js";
7
- import { ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
8
- import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
9
- import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
10
- export { ActorExit, type ActorRef, type ActorRefSync, ActorScope, ActorStoppedError, type ActorSystemService as ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type HandlerContext, type InspectionEvent, type InspectorService as Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, NoReplyError, PersistenceError, type ProcessEventResult, type Recovery, type RecoveryContext, type ReplyFields, type ReplyResult, type SimulationResult, type SpawnEvent, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
7
+ import { ActorClient, ActorLifecycle, ActorRef, ActorRefSync, ActorScope, ActorSystem, ActorSystemService, Default, SystemEvent, SystemEventListener, SystemSpawnOptions, TransitionInfo } from "./actor.js";
8
+ import { InputTestHarnessOptions, SimulationOptions, SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
9
+ import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
10
+ export { type ActorClient, ActorExit, type ActorExit as ActorExitType, type ActorLifecycle, type ActorRef, type ActorRefSync, ActorScope, ActorStoppedError, type ActorSystemService as ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type DefectPhase, type DeferReplyResult, DuplicateActorError, type Durability, type DurabilityCommit, type EffectEvent, type ErrorEvent, Event, type EventReceivedEvent, type FinalContext, type GuardEvent, type GuardPredicate, type HandlerContext, type InputMakeConfig, type InputTestHarnessOptions, type InspectionEvent, type InspectorService as Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, type Lifecycle, machine_d_exports as Machine, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, NoReplyError, type OperationEvent, PersistenceError, type ProcessEventResult, type Recovery, type RecoveryContext, type ReplayOptions, type ReplyFields, type ReplyResult, type SimulationOptions, type SimulationResult, type SpawnEvent, type SpawnOptions, State, type StateHandlerContext, type StopEvent, Supervision, type SystemEvent, type SystemEventListener, type SystemSpawnOptions, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TimeoutConfig, type TracingInspectorOptions, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
@@ -10,6 +10,7 @@ type ResolveType<T> = T extends Schema.Schema<infer A> ? A : T;
10
10
  interface SpawnEvent<S> {
11
11
  readonly type: "@machine.spawn";
12
12
  readonly actorId: string;
13
+ readonly generation: number;
13
14
  readonly initialState: S;
14
15
  readonly timestamp: number;
15
16
  }
@@ -19,6 +20,7 @@ interface SpawnEvent<S> {
19
20
  interface EventReceivedEvent<S, E> {
20
21
  readonly type: "@machine.event";
21
22
  readonly actorId: string;
23
+ readonly generation: number;
22
24
  readonly state: S;
23
25
  readonly event: E;
24
26
  readonly timestamp: number;
@@ -29,17 +31,40 @@ interface EventReceivedEvent<S, E> {
29
31
  interface TransitionEvent<S, E> {
30
32
  readonly type: "@machine.transition";
31
33
  readonly actorId: string;
34
+ readonly generation: number;
32
35
  readonly fromState: S;
33
36
  readonly toState: S;
34
37
  readonly event: E;
35
38
  readonly timestamp: number;
36
39
  }
40
+ /** Event emitted after a transition guard runs. */
41
+ interface GuardEvent<S, E> {
42
+ readonly type: "@machine.guard";
43
+ readonly actorId: string;
44
+ readonly generation: number;
45
+ readonly state: S;
46
+ readonly event: E;
47
+ readonly guard: string;
48
+ readonly result: boolean;
49
+ readonly timestamp: number;
50
+ }
51
+ /** Event emitted before an accepted transition handler runs. */
52
+ interface OperationEvent<S, E> {
53
+ readonly type: "@machine.operation";
54
+ readonly actorId: string;
55
+ readonly generation: number;
56
+ readonly operation: string;
57
+ readonly state: S;
58
+ readonly event: E;
59
+ readonly timestamp: number;
60
+ }
37
61
  /**
38
62
  * Event emitted when a spawn effect runs
39
63
  */
40
64
  interface EffectEvent<S> {
41
65
  readonly type: "@machine.effect";
42
66
  readonly actorId: string;
67
+ readonly generation: number;
43
68
  readonly effectType: "spawn";
44
69
  readonly state: S;
45
70
  readonly timestamp: number;
@@ -47,9 +72,10 @@ interface EffectEvent<S> {
47
72
  interface TaskEvent<S> {
48
73
  readonly type: "@machine.task";
49
74
  readonly actorId: string;
75
+ readonly generation: number;
50
76
  readonly state: S;
51
77
  readonly taskName?: string;
52
- readonly phase: "start" | "success" | "failure" | "interrupt";
78
+ readonly phase: "start" | "success" | "failure" | "defect" | "interrupt";
53
79
  readonly error?: string;
54
80
  readonly timestamp: number;
55
81
  }
@@ -59,6 +85,7 @@ interface TaskEvent<S> {
59
85
  interface ErrorEvent<S, E> {
60
86
  readonly type: "@machine.error";
61
87
  readonly actorId: string;
88
+ readonly generation: number;
62
89
  readonly phase: "transition" | "spawn";
63
90
  readonly state: S;
64
91
  readonly event: E;
@@ -71,13 +98,14 @@ interface ErrorEvent<S, E> {
71
98
  interface StopEvent<S> {
72
99
  readonly type: "@machine.stop";
73
100
  readonly actorId: string;
101
+ readonly generation: number;
74
102
  readonly finalState: S;
75
103
  readonly timestamp: number;
76
104
  }
77
105
  /**
78
106
  * Union of all inspection events
79
107
  */
80
- type InspectionEvent<S, E> = SpawnEvent<S> | EventReceivedEvent<S, E> | TransitionEvent<S, E> | EffectEvent<S> | TaskEvent<S> | ErrorEvent<S, E> | StopEvent<S>;
108
+ type InspectionEvent<S, E> = SpawnEvent<S> | EventReceivedEvent<S, E> | TransitionEvent<S, E> | GuardEvent<S, E> | OperationEvent<S, E> | EffectEvent<S> | TaskEvent<S> | ErrorEvent<S, E> | StopEvent<S>;
81
109
  /**
82
110
  * Convenience alias for untyped inspection events.
83
111
  * Useful for general-purpose inspectors that don't need specific state/event types.
@@ -148,4 +176,4 @@ declare const collectingInspector: <S extends {
148
176
  readonly _tag: string;
149
177
  }>(events: InspectionEvent<S, E>[]) => InspectorService<S, E>;
150
178
  //#endregion
151
- export { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector };
179
+ export { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, GuardEvent, InspectionEvent, Inspector, InspectorHandler, InspectorService, OperationEvent, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector };
@@ -29,6 +29,8 @@ const inspectionSpanName = (event) => {
29
29
  case "@machine.spawn": return `Machine.inspect ${event.initialState._tag}`;
30
30
  case "@machine.event": return `Machine.inspect ${event.event._tag}`;
31
31
  case "@machine.transition": return `Machine.inspect ${event.fromState._tag}->${event.toState._tag}`;
32
+ case "@machine.guard": return `Machine.inspect guard:${event.guard}`;
33
+ case "@machine.operation": return `Machine.inspect operation:${event.operation}`;
32
34
  case "@machine.effect": return `Machine.inspect ${event.effectType}`;
33
35
  case "@machine.task": return `Machine.inspect task:${event.phase}`;
34
36
  case "@machine.error": return `Machine.inspect ${event.phase}`;
@@ -40,6 +42,8 @@ const inspectionTraceName = (event) => {
40
42
  case "@machine.spawn": return `machine.spawn ${event.initialState._tag}`;
41
43
  case "@machine.event": return `machine.event ${event.event._tag}`;
42
44
  case "@machine.transition": return `machine.transition ${event.fromState._tag}->${event.toState._tag}`;
45
+ case "@machine.guard": return `machine.guard ${event.guard}`;
46
+ case "@machine.operation": return `machine.operation ${event.operation}`;
43
47
  case "@machine.effect": return `machine.effect ${event.effectType}`;
44
48
  case "@machine.task": {
45
49
  let taskSuffix = "";
@@ -53,6 +57,7 @@ const inspectionTraceName = (event) => {
53
57
  const inspectionAttributes = (event) => {
54
58
  const shared = {
55
59
  "machine.actor.id": event.actorId,
60
+ "machine.actor.generation": event.generation,
56
61
  "machine.inspection.type": event.type
57
62
  };
58
63
  switch (event.type) {
@@ -71,6 +76,19 @@ const inspectionAttributes = (event) => {
71
76
  "machine.state.to": event.toState._tag,
72
77
  "machine.event.tag": event.event._tag
73
78
  };
79
+ case "@machine.guard": return {
80
+ ...shared,
81
+ "machine.state.current": event.state._tag,
82
+ "machine.event.tag": event.event._tag,
83
+ "machine.guard.name": event.guard,
84
+ "machine.guard.result": event.result
85
+ };
86
+ case "@machine.operation": return {
87
+ ...shared,
88
+ "machine.state.current": event.state._tag,
89
+ "machine.event.tag": event.event._tag,
90
+ "machine.operation.name": event.operation
91
+ };
74
92
  case "@machine.effect": return {
75
93
  ...shared,
76
94
  "machine.state.current": event.state._tag,
@@ -110,6 +128,7 @@ const tracingInspector = (options) => ({ onInspect: (event) => {
110
128
  const currentSpan = yield* Effect.option(Effect.currentSpan);
111
129
  if (Option.isSome(currentSpan)) currentSpan.value.event(traceName, BigInt(event.timestamp) * 1000000n, {
112
130
  actorId: event.actorId,
131
+ generation: event.generation,
113
132
  inspectionType: event.type
114
133
  });
115
134
  })().pipe(Effect.withSpan(spanName ?? inspectionSpanName(event), { attributes }));
@@ -123,6 +142,8 @@ const consoleInspector = () => makeInspectorEffect((event) => {
123
142
  case "@machine.spawn": return Effect.log(`${prefix} spawned -> ${event.initialState._tag}`);
124
143
  case "@machine.event": return Effect.log(`${prefix} received ${event.event._tag} in ${event.state._tag}`);
125
144
  case "@machine.transition": return Effect.log(`${prefix} ${event.fromState._tag} -> ${event.toState._tag}`);
145
+ case "@machine.guard": return Effect.log(`${prefix} guard ${event.guard} -> ${String(event.result)}`);
146
+ case "@machine.operation": return Effect.log(`${prefix} operation ${event.operation} in ${event.state._tag}`);
126
147
  case "@machine.effect": return Effect.log(`${prefix} ${event.effectType} effect in ${event.state._tag}`);
127
148
  case "@machine.task": return Effect.log(`${prefix} task ${event.phase} ${event.taskName ?? "<unnamed>"} in ${event.state._tag}`);
128
149
  case "@machine.error": return Effect.log(`${prefix} error in ${event.phase} ${event.state._tag} - ${String(event.error)}`);
@@ -2,6 +2,6 @@ import { ProcessEventHooks } from "./transition.js";
2
2
  import { InspectorService } from "../inspection.js";
3
3
  //#region src/internal/inspection.d.ts
4
4
  /** Adapt the Inspector service to the transition kernel. */
5
- declare const makeInspectionHooks: <S, E>(actorId: string, inspector: InspectorService<S, E>) => ProcessEventHooks<S, E>;
5
+ declare const makeInspectionHooks: <S, E>(actorId: string, inspector: InspectorService<S, E>, getGeneration?: () => number) => ProcessEventHooks<S, E>;
6
6
  //#endregion
7
7
  export { makeInspectionHooks };
@@ -11,10 +11,30 @@ const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function
11
11
  if (Effect.isEffect(result)) yield* result.pipe(Effect.ignoreCause);
12
12
  });
13
13
  /** Adapt the Inspector service to the transition kernel. */
14
- const makeInspectionHooks = (actorId, inspector) => ({
14
+ const makeInspectionHooks = (actorId, inspector, getGeneration = () => 0) => ({
15
+ onGuard: (evaluation) => emitWithTimestamp(inspector, (timestamp) => ({
16
+ type: "@machine.guard",
17
+ actorId,
18
+ generation: getGeneration(),
19
+ state: evaluation.state,
20
+ event: evaluation.event,
21
+ guard: evaluation.guard,
22
+ result: evaluation.result,
23
+ timestamp
24
+ })),
25
+ onOperation: (operation) => emitWithTimestamp(inspector, (timestamp) => ({
26
+ type: "@machine.operation",
27
+ actorId,
28
+ generation: getGeneration(),
29
+ operation: operation.operation,
30
+ state: operation.state,
31
+ event: operation.event,
32
+ timestamp
33
+ })),
15
34
  onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
16
35
  type: "@machine.effect",
17
36
  actorId,
37
+ generation: getGeneration(),
18
38
  effectType: "spawn",
19
39
  state,
20
40
  timestamp
@@ -22,6 +42,7 @@ const makeInspectionHooks = (actorId, inspector) => ({
22
42
  onTransition: (from, to, event) => emitWithTimestamp(inspector, (timestamp) => ({
23
43
  type: "@machine.transition",
24
44
  actorId,
45
+ generation: getGeneration(),
25
46
  fromState: from,
26
47
  toState: to,
27
48
  event,
@@ -30,6 +51,7 @@ const makeInspectionHooks = (actorId, inspector) => ({
30
51
  onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
31
52
  type: "@machine.error",
32
53
  actorId,
54
+ generation: getGeneration(),
33
55
  phase: info.phase,
34
56
  state: info.state,
35
57
  event: info.event,
@@ -4,6 +4,7 @@ interface Transition<State, Event, R> {
4
4
  readonly eventTag: string;
5
5
  readonly handler: TransitionHandler<State, Event, State, R>;
6
6
  readonly reenter?: boolean;
7
+ readonly guard?: GuardPredicate<State, Event, never>;
7
8
  }
8
9
  interface SpawnEffect<State, Event, R> {
9
10
  readonly stateTag: string;
@@ -0,0 +1,21 @@
1
+ //#region src/internal/machine-initialization.d.ts
2
+ /** How a machine creates the initial state for one actor. */
3
+ type MachineInitialization<Input, State> = {
4
+ readonly _tag: "Static";
5
+ readonly state: State;
6
+ } | {
7
+ readonly _tag: "Input";
8
+ readonly initialize: (input: Input) => State;
9
+ };
10
+ declare const make: <Input, State>(initial: State | ((input: Input) => State)) => MachineInitialization<Input, State>;
11
+ /** Resolve one actor initial state. This is the only input erasure seam. */
12
+ declare const resolve: <Input, State>(initialization: MachineInitialization<Input, State>, input: unknown) => State;
13
+ /** Return the static value, or undefined for an input machine. */
14
+ declare const staticValue: <Input, State>(initialization: MachineInitialization<Input, State>) => State | undefined;
15
+ declare const MachineInitialization: {
16
+ make: typeof make;
17
+ resolve: typeof resolve;
18
+ staticValue: typeof staticValue;
19
+ };
20
+ //#endregion
21
+ export { MachineInitialization };
@@ -0,0 +1,27 @@
1
+ //#region src/internal/machine-initialization.ts
2
+ const make = (initial) => {
3
+ if (typeof initial === "function") return {
4
+ _tag: "Input",
5
+ initialize: initial
6
+ };
7
+ return {
8
+ _tag: "Static",
9
+ state: initial
10
+ };
11
+ };
12
+ /** Resolve one actor initial state. This is the only input erasure seam. */
13
+ const resolve = (initialization, input) => {
14
+ if (initialization._tag === "Static") return initialization.state;
15
+ return initialization.initialize(input);
16
+ };
17
+ /** Return the static value, or undefined for an input machine. */
18
+ const staticValue = (initialization) => {
19
+ if (initialization._tag === "Static") return initialization.state;
20
+ };
21
+ const MachineInitialization = {
22
+ make,
23
+ resolve,
24
+ staticValue
25
+ };
26
+ //#endregion
27
+ export { MachineInitialization };
@@ -1 +1,15 @@
1
- export {}
1
+ import { DefectPhase } from "../supervision.js";
2
+ //#region src/internal/runtime.d.ts
3
+ /** Internal exit reason for one runtime generation. */
4
+ type RuntimeExit<S> = {
5
+ readonly _tag: "Final";
6
+ readonly state: S;
7
+ } | {
8
+ readonly _tag: "Stopped";
9
+ } | {
10
+ readonly _tag: "Defect";
11
+ readonly cause: Cause.Cause<unknown>;
12
+ readonly phase: DefectPhase;
13
+ };
14
+ //#endregion
15
+ export { RuntimeExit };
@@ -2,7 +2,6 @@ import { INTERNAL_INIT_EVENT, isEffect } from "./utils.js";
2
2
  import { ActorStoppedError, NoReplyError } from "../errors.js";
3
3
  import { makeEventAdvancement } from "./event-advancement.js";
4
4
  import { processEventCoreImmediate, runSpawnEffects, shouldPostpone } from "./transition.js";
5
- import { ActorExit } from "../supervision.js";
6
5
  import { ActorSystem } from "../actor.js";
7
6
  import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
8
7
  //#region src/internal/runtime.ts
@@ -18,12 +17,24 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
18
17
  * - Reply settlement (call/ask Deferreds)
19
18
  * - Reply schema validation
20
19
  * - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
21
- * - ActorExit with exit reason (Final/Stopped/Defect) via exitDeferred
20
+ * - RuntimeExit with exit reason (Final/Stopped/Defect) via exitDeferred
22
21
  *
23
22
  * Used by entity-machine and local actor (actor.ts delegates here).
24
23
  *
25
24
  * @internal
26
25
  */
26
+ const RuntimeExit = {
27
+ Final: (state) => ({
28
+ _tag: "Final",
29
+ state
30
+ }),
31
+ Stopped: { _tag: "Stopped" },
32
+ Defect: (cause, phase) => ({
33
+ _tag: "Defect",
34
+ cause,
35
+ phase
36
+ })
37
+ };
27
38
  /**
28
39
  * Create a runtime for a machine. Returns a handle for sending events
29
40
  * and querying state. The runtime owns:
@@ -41,6 +52,7 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
41
52
  */
42
53
  const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
43
54
  const { actorId, hooks, lifecycle } = config;
55
+ const generation = config.generation ?? 0;
44
56
  const services = yield* Effect.context();
45
57
  const fork = Effect.runForkWith(services);
46
58
  const { stateRef, stoppedRef, eventQueue } = config.cellResources;
@@ -84,51 +96,78 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
84
96
  yield* Deferred.await(startDeferred);
85
97
  return;
86
98
  }
99
+ const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
100
+ const loopFiber = loopFiberRef.current;
101
+ if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
102
+ return Effect.void;
103
+ })), Effect.asVoid);
104
+ const initialState = yield* SubscriptionRef.get(stateRef);
105
+ const initialProcessing = processEventCoreImmediate(machine, initialState, initEvent, self, stateScopeRef, system, actorId, {
106
+ ...hooks,
107
+ onSpawnDefect: initialSpawnDefectSignal
108
+ }, generation);
109
+ let initialResult;
110
+ if (isEffect(initialProcessing)) initialResult = yield* initialProcessing.pipe(Effect.catchCause((cause) => Effect.gen(function* () {
111
+ yield* Ref.set(stoppedRef, true);
112
+ yield* Scope.close(stateScopeRef.current, Exit.void);
113
+ yield* Scope.close(actorScope, Exit.void);
114
+ yield* Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "transition"));
115
+ return yield* Effect.failCause(cause);
116
+ })));
117
+ else initialResult = initialProcessing;
118
+ if (initialResult.transitioned) {
119
+ yield* SubscriptionRef.set(stateRef, initialResult.newState);
120
+ if (lifecycle?.onStateChange !== void 0) {
121
+ const stateChange = lifecycle.onStateChange(initialResult, initEvent);
122
+ if (isEffect(stateChange)) yield* stateChange;
123
+ }
124
+ if (lifecycle?.onProcessed !== void 0) {
125
+ const processed = lifecycle.onProcessed(initialResult, initEvent);
126
+ if (isEffect(processed)) yield* processed;
127
+ }
128
+ }
129
+ const stableInitialState = initialResult.newState;
87
130
  const backgroundFibers = [];
88
131
  for (const bg of machine._backgroundEffectEntries()) {
89
132
  const fiber = yield* bg.handler({
90
133
  actorId,
91
- state: machine.initial,
134
+ generation,
135
+ state: stableInitialState,
92
136
  event: initEvent,
93
137
  self,
94
138
  system
95
139
  }).pipe(Effect.forkIn(actorScope));
96
140
  backgroundFibers.push(fiber);
97
141
  }
98
- if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
99
- const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
100
- const loopFiber = loopFiberRef.current;
101
- if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
102
- return Effect.void;
103
- })), Effect.asVoid);
104
- yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchCause((cause) => Effect.gen(function* () {
142
+ if (!initialResult.lifecycleRan && lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(stableInitialState);
143
+ if (!initialResult.lifecycleRan) yield* runSpawnEffects(machine, stableInitialState, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal, generation).pipe(Effect.catchCause((cause) => Effect.gen(function* () {
105
144
  yield* Ref.set(stoppedRef, true);
106
145
  yield* Scope.close(stateScopeRef.current, Exit.void);
107
146
  yield* Scope.close(actorScope, Exit.void);
108
- yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
147
+ yield* Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "initial-spawn"));
109
148
  return yield* Effect.failCause(cause);
110
149
  })));
111
- if (machine._isFinal(machine.initial._tag)) {
112
- if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
150
+ if (machine._isFinal(stableInitialState._tag)) {
151
+ if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(stableInitialState);
113
152
  yield* Ref.set(stoppedRef, true);
114
153
  yield* Scope.close(stateScopeRef.current, Exit.void);
115
154
  yield* Scope.close(actorScope, Exit.void);
116
- yield* setExit(ActorExit.Final(machine.initial));
155
+ yield* setExit(RuntimeExit.Final(stableInitialState));
117
156
  yield* Deferred.succeed(startDeferred, void 0);
118
157
  return;
119
158
  }
120
159
  const augmentedHooks = {
121
160
  ...hooks,
122
- onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
161
+ onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
123
162
  const loopFiber = loopFiberRef.current;
124
163
  if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
125
164
  return Effect.void;
126
165
  })), Effect.asVoid)
127
166
  };
128
- const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
167
+ const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
129
168
  loopFiberRef.current = loopFiber;
130
169
  if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
131
- if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
170
+ if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) return setExit(RuntimeExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
132
171
  return Effect.never;
133
172
  })))).pipe(Effect.forkIn(actorScope));
134
173
  yield* Effect.forkDetach(Effect.gen(function* () {
@@ -137,7 +176,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
137
176
  else yield* Scope.close(actorScope, loopExit);
138
177
  }));
139
178
  yield* Deferred.succeed(startDeferred, void 0);
140
- }).pipe(Effect.catchCause((cause) => Deferred.failCause(startDeferred, cause).pipe(Effect.andThen(Effect.failCause(cause)))));
179
+ }).pipe(Effect.catchCause((cause) => Ref.set(stoppedRef, true).pipe(Effect.andThen(Scope.close(stateScopeRef.current, Exit.void)), Effect.andThen(Scope.close(actorScope, Exit.void)), Effect.andThen(setExit(RuntimeExit.Defect(cause, "transition"))), Effect.andThen(Deferred.failCause(startDeferred, cause)), Effect.andThen(Effect.failCause(cause)))));
141
180
  const stop = Effect.gen(function* () {
142
181
  if (yield* Ref.get(stoppedRef)) return;
143
182
  if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
@@ -147,7 +186,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
147
186
  if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
148
187
  yield* Scope.close(stateScopeRef.current, Exit.void);
149
188
  yield* Scope.close(actorScope, Exit.void);
150
- yield* setExit(ActorExit.Stopped);
189
+ yield* setExit(RuntimeExit.Stopped);
151
190
  }).pipe(Effect.asVoid);
152
191
  if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
153
192
  return {
@@ -233,7 +272,7 @@ const settlePendingRequests = (pendingRequests, actorId) => Effect.gen(function*
233
272
  for (const settle of pendingRequests) yield* settle(error);
234
273
  pendingRequests.clear();
235
274
  });
236
- const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
275
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
237
276
  const forkEffect = fork ?? Effect.runFork;
238
277
  /** Set the exit deferred exactly once. */
239
278
  const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
@@ -248,7 +287,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
248
287
  hasReply: false,
249
288
  deferReply: false,
250
289
  reply: void 0,
251
- postponed: true
290
+ postponed: true,
291
+ transitions: []
252
292
  };
253
293
  let input = queued;
254
294
  if (queued._tag === "call") {
@@ -276,7 +316,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
276
316
  const processQueued = (currentState, queued) => Effect.gen(function* () {
277
317
  const event = queued.event;
278
318
  if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
279
- const processing = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
319
+ const processing = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation);
280
320
  let result;
281
321
  if (isEffect(processing)) result = yield* processing;
282
322
  else result = processing;
@@ -364,7 +404,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
364
404
  hasReply: false,
365
405
  deferReply: false,
366
406
  reply: void 0,
367
- postponed: false
407
+ postponed: false,
408
+ transitions: []
368
409
  }));
369
410
  }
370
411
  yield* Scope.close(stateScopeRef.current, Exit.void);
@@ -373,7 +414,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
373
414
  while (true) {
374
415
  const queued = yield* Queue.take(eventQueue);
375
416
  if (queued._tag === "drain") {
376
- yield* shutdown(ActorExit.Stopped);
417
+ yield* shutdown(RuntimeExit.Stopped);
377
418
  yield* Deferred.succeed(queued.done, void 0);
378
419
  return;
379
420
  }
@@ -382,7 +423,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
382
423
  if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
383
424
  else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
384
425
  else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
385
- return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
426
+ return shutdown(RuntimeExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
386
427
  }));
387
428
  let stopped;
388
429
  if (advancement === void 0) {
@@ -392,7 +433,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
392
433
  } else stopped = (yield* catchEventDefect(advancement.advance(eventQueued))).stopped;
393
434
  if (stopped) {
394
435
  const finalState = yield* SubscriptionRef.get(stateRef);
395
- yield* shutdown(ActorExit.Final(finalState));
436
+ yield* shutdown(RuntimeExit.Final(finalState));
396
437
  return;
397
438
  }
398
439
  }
@@ -1,10 +1,35 @@
1
1
  import { Machine } from "../machine.js";
2
2
  import { Transition } from "./machine-definition.js";
3
3
  //#region src/internal/transition.d.ts
4
+ interface ExecutedStep<S, E> {
5
+ readonly previousState: S;
6
+ readonly newState: S;
7
+ readonly event: E;
8
+ readonly transition: Transition<S, E, never>;
9
+ }
10
+ interface ExecutedTransition<S, E = unknown> {
11
+ readonly newState: S;
12
+ readonly transitioned: boolean;
13
+ readonly reenter: boolean;
14
+ readonly hasReply: boolean;
15
+ readonly deferReply: boolean;
16
+ readonly reply: unknown;
17
+ readonly transition?: Transition<S, E, never>;
18
+ readonly steps: ReadonlyArray<ExecutedStep<S, E>>;
19
+ }
20
+ declare const executeTransitionImmediate: <S extends {
21
+ readonly _tag: string;
22
+ }, E extends {
23
+ readonly _tag: string;
24
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E, hooks?: ProcessEventHooks<S, E>) => ExecutedTransition<S, E> | Effect.Effect<ExecutedTransition<S, E>>;
4
25
  /**
5
26
  * Optional hooks for event processing inspection/tracing.
6
27
  */
7
28
  interface ProcessEventHooks<S, E> {
29
+ /** Called after each guard candidate is evaluated. */
30
+ readonly onGuard?: (evaluation: GuardEvaluation<S, E>) => Effect.Effect<void>;
31
+ /** Called before an accepted transition handler runs. */
32
+ readonly onOperation?: (operation: TransitionOperation<S, E>) => Effect.Effect<void>;
8
33
  /** Called before running spawn effects */
9
34
  readonly onSpawnEffect?: (state: S) => Effect.Effect<void>;
10
35
  /** Called after transition completes */
@@ -14,6 +39,17 @@ interface ProcessEventHooks<S, E> {
14
39
  /** Called when a forked spawn fiber defects — signals the runtime to set exitDeferred */
15
40
  readonly onSpawnDefect?: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
16
41
  }
42
+ interface GuardEvaluation<S, E> {
43
+ readonly guard: string;
44
+ readonly state: S;
45
+ readonly event: E;
46
+ readonly result: boolean;
47
+ }
48
+ interface TransitionOperation<S, E> {
49
+ readonly operation: string;
50
+ readonly state: S;
51
+ readonly event: E;
52
+ }
17
53
  /**
18
54
  * Error info for inspection hooks.
19
55
  */
@@ -26,7 +62,7 @@ interface ProcessEventError<S, E> {
26
62
  /**
27
63
  * Result of processing an event through the machine.
28
64
  */
29
- interface ProcessEventResult<S> {
65
+ interface ProcessEventResult<S, E = unknown> {
30
66
  /** New state after processing */
31
67
  readonly newState: S;
32
68
  /** Previous state before processing */
@@ -45,6 +81,12 @@ interface ProcessEventResult<S> {
45
81
  readonly reply?: unknown;
46
82
  /** Whether the event was postponed (buffered for retry after next state change) */
47
83
  readonly postponed: boolean;
84
+ /** Each accepted edge in the stable macrostep. */
85
+ readonly transitions: ReadonlyArray<{
86
+ readonly previousState: S;
87
+ readonly newState: S;
88
+ readonly event: E;
89
+ }>;
48
90
  }
49
91
  /**
50
92
  * Resolve which transition should fire for a given state and event.
@@ -54,6 +96,12 @@ declare const resolveTransition: <S extends {
54
96
  readonly _tag: string;
55
97
  }, E extends {
56
98
  readonly _tag: string;
57
- }, R>(machine: Machine<S, E, R, any, any>, currentState: S, event: E) => Transition<S, E, never> | undefined;
99
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => Transition<S, E, never> | undefined;
100
+ /** Resolve a transition with pure or Effect guards. */
101
+ declare const resolveTransitionEffect: <S extends {
102
+ readonly _tag: string;
103
+ }, E extends {
104
+ readonly _tag: string;
105
+ }, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => Effect.Effect<Transition<S, E, never> | undefined>;
58
106
  //#endregion
59
- export { ProcessEventError, ProcessEventHooks, ProcessEventResult, resolveTransition };
107
+ export { GuardEvaluation, ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionOperation, executeTransitionImmediate, resolveTransition, resolveTransitionEffect };