effect-machine 0.11.0 → 0.13.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 (68) hide show
  1. package/README.md +128 -324
  2. package/dist/actor.d.ts +52 -31
  3. package/dist/actor.js +218 -283
  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 +178 -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 +25 -17
  17. package/dist/errors.js +10 -5
  18. package/dist/index.d.ts +6 -4
  19. package/dist/index.js +4 -3
  20. package/dist/internal/brands.d.ts +14 -1
  21. package/dist/internal/runtime.d.ts +142 -0
  22. package/dist/internal/runtime.js +357 -0
  23. package/dist/internal/transition.d.ts +10 -4
  24. package/dist/internal/transition.js +24 -12
  25. package/dist/internal/utils.d.ts +42 -6
  26. package/dist/internal/utils.js +27 -1
  27. package/dist/machine.d.ts +89 -55
  28. package/dist/machine.js +80 -68
  29. package/dist/schema.d.ts +35 -34
  30. package/dist/schema.js +33 -4
  31. package/dist/supervision.d.ts +97 -0
  32. package/dist/supervision.js +42 -0
  33. package/dist/testing.d.ts +17 -8
  34. package/dist/testing.js +22 -23
  35. package/package.json +7 -7
  36. package/v3/dist/actor.d.ts +54 -37
  37. package/v3/dist/actor.js +209 -277
  38. package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
  39. package/v3/dist/cluster/adapters/in-memory.js +62 -0
  40. package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
  41. package/v3/dist/cluster/entity-actor-ref.js +19 -0
  42. package/v3/dist/cluster/entity-machine.d.ts +34 -49
  43. package/v3/dist/cluster/entity-machine.js +134 -50
  44. package/v3/dist/cluster/index.d.ts +5 -2
  45. package/v3/dist/cluster/index.js +4 -1
  46. package/v3/dist/cluster/persistence.d.ts +48 -0
  47. package/v3/dist/cluster/persistence.js +14 -0
  48. package/v3/dist/cluster/to-entity.d.ts +5 -2
  49. package/v3/dist/cluster/to-entity.js +12 -4
  50. package/v3/dist/errors.d.ts +18 -8
  51. package/v3/dist/errors.js +9 -4
  52. package/v3/dist/index.d.ts +6 -4
  53. package/v3/dist/index.js +3 -2
  54. package/v3/dist/internal/brands.d.ts +15 -1
  55. package/v3/dist/internal/runtime.d.ts +142 -0
  56. package/v3/dist/internal/runtime.js +335 -0
  57. package/v3/dist/internal/transition.d.ts +10 -4
  58. package/v3/dist/internal/transition.js +23 -11
  59. package/v3/dist/internal/utils.d.ts +42 -6
  60. package/v3/dist/internal/utils.js +27 -1
  61. package/v3/dist/machine.d.ts +35 -47
  62. package/v3/dist/machine.js +62 -64
  63. package/v3/dist/schema.d.ts +35 -34
  64. package/v3/dist/schema.js +29 -3
  65. package/v3/dist/supervision.d.ts +97 -0
  66. package/v3/dist/supervision.js +42 -0
  67. package/v3/dist/testing.d.ts +18 -9
  68. package/v3/dist/testing.js +21 -22
package/v3/dist/errors.js CHANGED
@@ -12,12 +12,10 @@ import { Schema } from "effect";
12
12
  */
13
13
  /** Attempted to spawn/restore actor with ID already in use */
14
14
  var DuplicateActorError = class extends Schema.TaggedError()("DuplicateActorError", { actorId: Schema.String }) {};
15
- /** Machine has unprovided effect slots */
16
- var UnprovidedSlotsError = class extends Schema.TaggedError()("UnprovidedSlotsError", { slots: Schema.Array(Schema.String) }) {};
17
15
  /** Operation requires schemas attached to machine */
18
16
  var MissingSchemaError = class extends Schema.TaggedError()("MissingSchemaError", { operation: Schema.String }) {};
19
17
  /** State/Event schema has no variants */
20
- var InvalidSchemaError = class extends Schema.TaggedError()("InvalidSchemaError", {}) {};
18
+ var InvalidSchemaError = class extends Schema.TaggedError()("InvalidSchemaError", { message: Schema.String }) {};
21
19
  /** $match called with missing handler for tag */
22
20
  var MissingMatchHandlerError = class extends Schema.TaggedError()("MissingMatchHandlerError", { tag: Schema.String }) {};
23
21
  /** Slot handler not found at runtime (internal error) */
@@ -39,5 +37,12 @@ var NoReplyError = class extends Schema.TaggedError()("NoReplyError", {
39
37
  actorId: Schema.String,
40
38
  eventTag: Schema.String
41
39
  }) {};
40
+ /** Persistence adapter operation failed */
41
+ var PersistenceError = class extends Schema.TaggedError()("PersistenceError", { message: Schema.String }) {};
42
+ /** Optimistic locking failure — stored version doesn't match expected */
43
+ var VersionConflictError = class extends Schema.TaggedError()("VersionConflictError", {
44
+ expected: Schema.Number,
45
+ actual: Schema.Number
46
+ }) {};
42
47
  //#endregion
43
- export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
48
+ export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError };
@@ -1,9 +1,11 @@
1
+ import { ActorExit, CellPhase, DefectPhase, Supervision } from "./supervision.js";
2
+ import { ReplyResult } from "./internal/utils.js";
3
+ import { Event, MachineEventSchema, MachineStateSchema, ReplyFields, State } from "./schema.js";
4
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError } from "./errors.js";
1
5
  import { EffectHandlers, EffectSlot, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlot, GuardSlots, GuardsDef, GuardsSchema, MachineContext, Slot } from "./slot.js";
2
- import { Event, MachineEventSchema, MachineStateSchema, State } from "./schema.js";
3
- import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
4
6
  import { ProcessEventResult } from "./internal/transition.js";
5
- import { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SpawnEffect, StateHandlerContext, TaskOptions, Transition, machine_d_exports } from "./machine.js";
7
+ import { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, machine_d_exports } from "./machine.js";
6
8
  import { ActorRef, ActorRefSync, ActorSystem, Default, SystemEvent, SystemEventListener, TransitionInfo } from "./actor.js";
7
9
  import { SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
8
10
  import { AnyInspectionEvent, EffectEvent, ErrorEvent, EventReceivedEvent, InspectionEvent, Inspector, InspectorHandler, SpawnEvent, StopEvent, TaskEvent, TracingInspectorOptions, TransitionEvent, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
9
- export { type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type BuiltMachine, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type SimulationResult, Slot, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, type SpawnEvent, State, type StateHandlerContext, type StopEvent, type SystemEvent, type SystemEventListener, type TaskEvent, type TaskOptions, type TestHarness, type TestHarnessOptions, type TracingInspectorOptions, type Transition, type TransitionEvent, type TransitionInfo, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
11
+ export { ActorExit, type ActorRef, type ActorRefSync, ActorStoppedError, type ActorSystem, Default as ActorSystemDefault, ActorSystem as ActorSystemService, type AnyInspectionEvent, AssertionError, type BackgroundEffect, type CellPhase, type DefectPhase, DuplicateActorError, type EffectEvent, type EffectSlots, type EffectsDef, type EffectsSchema, type ErrorEvent, Event, type EventReceivedEvent, type GuardHandlers, type GuardSlot, type GuardSlots, type GuardsDef, type GuardsSchema, type HandlerContext, type InspectionEvent, type Inspector, type InspectorHandler, Inspector as InspectorService, InvalidSchemaError, machine_d_exports as Machine, type MachineContext, type MachineEventSchema, type MachineRef, type MachineStateSchema, type Machine as MachineType, type MakeConfig, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, type ProcessEventResult, type ProvideHandlers, ProvisionValidationError, type ReplyFields, type ReplyResult, type SimulationResult, Slot, type SlotContext, type EffectHandlers as SlotEffectHandlers, type EffectSlot as SlotEffectSlot, SlotProvisionError, type SpawnEffect, 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 Transition, type TransitionEvent, type TransitionInfo, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
package/v3/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, SlotProvisionError, VersionConflictError } from "./errors.js";
1
2
  import { Inspector, collectingInspector, combineInspectors, consoleInspector, makeInspector, makeInspectorEffect, tracingInspector } from "./inspection.js";
2
- import { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError } from "./errors.js";
3
3
  import { Slot } from "./slot.js";
4
4
  import { machine_exports } from "./machine.js";
5
+ import { ActorExit, Supervision } from "./supervision.js";
5
6
  import { ActorSystem, Default } from "./actor.js";
6
7
  import { Event, State } from "./schema.js";
7
8
  import { assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate } from "./testing.js";
8
- export { ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, Slot, SlotProvisionError, State, UnprovidedSlotsError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
9
+ export { ActorExit, ActorStoppedError, Default as ActorSystemDefault, ActorSystem as ActorSystemService, AssertionError, DuplicateActorError, Event, Inspector as InspectorService, InvalidSchemaError, machine_exports as Machine, MissingMatchHandlerError, MissingSchemaError, NoReplyError, PersistenceError, ProvisionValidationError, Slot, SlotProvisionError, State, Supervision, VersionConflictError, assertNeverReaches, assertPath, assertReaches, collectingInspector, combineInspectors, consoleInspector, createTestHarness, makeInspector, makeInspectorEffect, simulate, tracingInspector };
@@ -29,6 +29,20 @@ type FullStateBrand<D extends Record<string, unknown>> = StateBrand & SchemaIdBr
29
29
  * Full event brand: combines base event brand with schema-specific brand
30
30
  */
31
31
  type FullEventBrand<D extends Record<string, unknown>> = EventBrand & SchemaIdBrand<D>;
32
+ /**
33
+ * Brand that carries the reply type for an event variant.
34
+ * Present only on events defined with Event.reply().
35
+ */
36
+ declare const ReplyTypeId: unique symbol;
37
+ type ReplyTypeId = typeof ReplyTypeId;
38
+ interface ReplyTypeBrand<R> extends Brand.Brand<ReplyTypeId> {
39
+ readonly _ReplyType: R;
40
+ }
41
+ /**
42
+ * Extract the reply type from a branded event value.
43
+ * Returns `never` if the event has no reply schema.
44
+ */
45
+ type ExtractReply<E> = E extends ReplyTypeBrand<infer R> ? R : never;
32
46
  /**
33
47
  * Value or constructor for a tagged type.
34
48
  * Accepts both plain values (empty structs) and constructor functions (non-empty structs).
@@ -37,4 +51,4 @@ type TaggedOrConstructor<T extends {
37
51
  readonly _tag: string;
38
52
  }> = T | ((...args: never[]) => T);
39
53
  //#endregion
40
- export { BrandedEvent, BrandedState, EventBrand, EventTypeId, FullEventBrand, FullStateBrand, SchemaIdBrand, StateBrand, StateTypeId, TaggedOrConstructor };
54
+ export { BrandedEvent, BrandedState, EventBrand, EventTypeId, ExtractReply, FullEventBrand, FullStateBrand, ReplyTypeBrand, ReplyTypeId, SchemaIdBrand, StateBrand, StateTypeId, TaggedOrConstructor };
@@ -0,0 +1,142 @@
1
+ import { ActorExit } from "../supervision.js";
2
+ import { NoReplyError } from "../errors.js";
3
+ import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
4
+ import { ProcessEventHooks, ProcessEventResult } from "./transition.js";
5
+ import { Machine, MachineRef } from "../machine.js";
6
+ import { ActorSystem } from "../actor.js";
7
+ import { Deferred, Effect, Queue, Ref, Scope, SubscriptionRef } from "effect";
8
+
9
+ //#region src/internal/runtime.d.ts
10
+ /** @internal */
11
+ type RuntimeQueuedEvent<E> = {
12
+ readonly _tag: "send";
13
+ readonly event: E;
14
+ } | {
15
+ readonly _tag: "sendWait";
16
+ readonly event: E;
17
+ readonly done: Deferred.Deferred<void>;
18
+ } | {
19
+ readonly _tag: "call";
20
+ readonly event: E;
21
+ readonly reply: Deferred.Deferred<ProcessEventResult<{
22
+ readonly _tag: string;
23
+ }>, unknown>;
24
+ } | {
25
+ readonly _tag: "ask";
26
+ readonly event: E;
27
+ readonly reply: Deferred.Deferred<unknown, NoReplyError>;
28
+ } | {
29
+ readonly _tag: "drain";
30
+ readonly done: Deferred.Deferred<void, never>;
31
+ };
32
+ /**
33
+ * Resources owned by the actor cell (stable across generations).
34
+ * When provided, createRuntime uses these instead of allocating its own.
35
+ * @internal
36
+ */
37
+ interface RuntimeCellResources<S, E> {
38
+ readonly stateRef: SubscriptionRef.SubscriptionRef<S>;
39
+ readonly eventQueue: Queue.Queue<RuntimeQueuedEvent<E>>;
40
+ readonly stoppedRef: Ref.Ref<boolean>;
41
+ }
42
+ /** @internal */
43
+ interface RuntimeHandle<S, E> {
44
+ /** Enqueue a fire-and-forget event */
45
+ readonly send: (event: E) => Effect.Effect<void>;
46
+ /** Enqueue event and wait for processing to complete (for RPC Send) */
47
+ readonly sendWait: (event: E) => Effect.Effect<void>;
48
+ /** Enqueue an ask event, returns the reply value */
49
+ readonly ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
50
+ /** Get current state */
51
+ readonly getState: Effect.Effect<S>;
52
+ /** SubscriptionRef for state observation */
53
+ readonly stateRef: SubscriptionRef.SubscriptionRef<S>;
54
+ /** Whether the runtime has stopped (final state reached) */
55
+ readonly isStopped: Effect.Effect<boolean>;
56
+ /** Stop the runtime (interrupt event loop, clean up) */
57
+ readonly stop: Effect.Effect<void>;
58
+ /** @internal — raw event queue for direct enqueue (actor.ts uses this for pendingReplies tracking) */
59
+ readonly _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
60
+ /** @internal — stopped ref for direct access */
61
+ readonly _stoppedRef: Ref.Ref<boolean>;
62
+ /**
63
+ * Exit deferred — set exactly once with the exit reason when the runtime stops.
64
+ * Final state → ActorExit.Final, explicit stop → ActorExit.Stopped, defect → ActorExit.Defect.
65
+ */
66
+ readonly exitDeferred: Deferred.Deferred<ActorExit<S>, never>;
67
+ /**
68
+ * Actor scope — owns background fibers for this generation.
69
+ * Closing this scope interrupts all background fibers.
70
+ */
71
+ readonly actorScope: Scope.CloseableScope;
72
+ }
73
+ /** @internal */
74
+ interface RuntimeLifecycleHooks<S, E> {
75
+ /** Before processEventCore — actor emits @machine.event inspection */
76
+ readonly onEvent?: (state: S, event: E) => Effect.Effect<void>;
77
+ /** After SubscriptionRef.set on transition — actor notifies listeners, annotates spans */
78
+ readonly onStateChange?: (result: ProcessEventResult<S>, event: E) => Effect.Effect<void>;
79
+ /** After reply settlement when transition occurred — actor publishes to transitionsPubSub */
80
+ readonly onProcessed?: (result: ProcessEventResult<S>, event: E) => Effect.Effect<void>;
81
+ /** When final state detected in event loop — actor emits @machine.stop */
82
+ readonly onFinal?: (state: S) => Effect.Effect<void>;
83
+ /** Before stop resource cleanup — actor emits @machine.stop, settles pending replies */
84
+ readonly onShutdown?: () => Effect.Effect<void>;
85
+ /** Before initial spawn effects — actor emits @machine.effect inspection */
86
+ readonly onInitialSpawnEffects?: (state: S) => Effect.Effect<void>;
87
+ }
88
+ /** @internal */
89
+ interface RuntimeConfig<S, E> {
90
+ readonly actorId: string;
91
+ readonly hooks?: ProcessEventHooks<S, E>;
92
+ /**
93
+ * Cell-owned resources. When provided, the runtime uses the cell's stateRef,
94
+ * eventQueue, and stoppedRef instead of creating its own.
95
+ * Used by actor.ts for supervision (cell owns stable resources across generations).
96
+ */
97
+ readonly cellResources?: RuntimeCellResources<S, E>;
98
+ /**
99
+ * Custom queue factory. Default: `Queue.unbounded()`.
100
+ * Use `Queue.sliding(n)` or `Queue.dropping(n)` for bounded queues.
101
+ * Ignored when cellResources is provided.
102
+ */
103
+ readonly queueFactory?: Effect.Effect<Queue.Queue<RuntimeQueuedEvent<E>>>;
104
+ /** Lifecycle callbacks for actor-specific concerns */
105
+ readonly lifecycle?: RuntimeLifecycleHooks<S, E>;
106
+ /** Wrap each processQueued invocation — actor uses for span annotations */
107
+ readonly wrapProcess?: (state: S, event: E, inner: Effect.Effect<ProcessQueuedResult<S>>) => Effect.Effect<ProcessQueuedResult<S>>;
108
+ /** Called after self.spawn succeeds — actor tracks children */
109
+ readonly onChildSpawned?: (childId: string, child: unknown) => Effect.Effect<void>;
110
+ /** Skip registering stop as scope finalizer — actor manages its own lifecycle */
111
+ readonly skipFinalizer?: boolean;
112
+ /** Prefix for child actor IDs in self.spawn. Entity-machine uses `${actorId}/`. Default: no prefix. */
113
+ readonly childIdPrefix?: string;
114
+ }
115
+ /** @internal */
116
+ interface ProcessQueuedResult<S> {
117
+ readonly shouldStop: boolean;
118
+ readonly stateChanged: boolean;
119
+ readonly result: ProcessEventResult<S>;
120
+ }
121
+ /**
122
+ * Create a runtime for a machine. Returns a handle for sending events
123
+ * and querying state. The runtime owns:
124
+ * - Event loop fiber
125
+ * - Postpone buffer
126
+ * - Background effects
127
+ * - State scope (spawn effects)
128
+ * - Final state detection
129
+ * - Exit reason via exitDeferred
130
+ *
131
+ * Resources (stateRef, eventQueue, stoppedRef) are either cell-provided
132
+ * or allocated fresh by the runtime.
133
+ *
134
+ * @internal
135
+ */
136
+ declare const createRuntime: <S extends {
137
+ readonly _tag: string;
138
+ }, E extends {
139
+ readonly _tag: string;
140
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<RuntimeHandle<S, E>, never, Scope.Scope | Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
141
+ //#endregion
142
+ export { ProcessQueuedResult, RuntimeCellResources, RuntimeConfig, RuntimeHandle, RuntimeLifecycleHooks, RuntimeQueuedEvent, createRuntime };
@@ -0,0 +1,335 @@
1
+ import { INTERNAL_INIT_EVENT } from "./utils.js";
2
+ import { NoReplyError } from "../errors.js";
3
+ import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
4
+ import { ActorExit } from "../supervision.js";
5
+ import { ActorSystem } from "../actor.js";
6
+ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
7
+ //#region src/internal/runtime.ts
8
+ /**
9
+ * Shared runtime kernel for machine event processing.
10
+ *
11
+ * Provides a single-queue event loop with:
12
+ * - Sequential event processing (no split-mailbox race)
13
+ * - Postpone buffer with drain-on-state-change (gen_statem)
14
+ * - Background effect lifecycle (under actorScope fault boundary)
15
+ * - Spawn effect lifecycle (per-state scope)
16
+ * - Final state detection → stop
17
+ * - Reply settlement (call/ask Deferreds)
18
+ * - Reply schema validation
19
+ * - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
20
+ * - ActorExit with exit reason (Final/Stopped/Defect) via exitDeferred
21
+ *
22
+ * Used by entity-machine and local actor (actor.ts delegates here).
23
+ *
24
+ * @internal
25
+ */
26
+ /**
27
+ * Create a runtime for a machine. Returns a handle for sending events
28
+ * and querying state. The runtime owns:
29
+ * - Event loop fiber
30
+ * - Postpone buffer
31
+ * - Background effects
32
+ * - State scope (spawn effects)
33
+ * - Final state detection
34
+ * - Exit reason via exitDeferred
35
+ *
36
+ * Resources (stateRef, eventQueue, stoppedRef) are either cell-provided
37
+ * or allocated fresh by the runtime.
38
+ *
39
+ * @internal
40
+ */
41
+ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
42
+ const { actorId, hooks, lifecycle } = config;
43
+ const stateRef = config.cellResources?.stateRef ?? (yield* SubscriptionRef.make(machine.initial));
44
+ const stoppedRef = config.cellResources?.stoppedRef ?? (yield* Ref.make(false));
45
+ const eventQueue = config.cellResources?.eventQueue ?? (yield* config.queueFactory ?? Queue.unbounded());
46
+ const exitDeferred = yield* Deferred.make();
47
+ const actorScope = yield* Scope.make();
48
+ const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
49
+ if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
50
+ _tag: "send",
51
+ event
52
+ });
53
+ });
54
+ const childPrefix = config.childIdPrefix ?? "";
55
+ const defaultSpawn = (childId, childMachine) => system.spawn(`${childPrefix}${childId}`, childMachine).pipe(Effect.provideService(ActorSystem, system));
56
+ const onChildSpawned = config.onChildSpawned;
57
+ const self = {
58
+ send: selfSend,
59
+ cast: selfSend,
60
+ spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn
61
+ };
62
+ const stateScopeRef = { current: yield* Scope.make() };
63
+ const backgroundFibers = [];
64
+ const initEvent = { _tag: INTERNAL_INIT_EVENT };
65
+ const ctx = {
66
+ actorId,
67
+ state: machine.initial,
68
+ event: initEvent,
69
+ self,
70
+ system
71
+ };
72
+ const { effects: effectSlots } = machine._slots;
73
+ for (const bg of machine.backgroundEffects) {
74
+ const fiber = yield* Effect.forkDaemon(bg.handler({
75
+ actorId,
76
+ state: machine.initial,
77
+ event: initEvent,
78
+ self,
79
+ effects: effectSlots,
80
+ system
81
+ }).pipe(Effect.provideService(machine.Context, ctx)));
82
+ backgroundFibers.push(fiber);
83
+ }
84
+ if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
85
+ const loopFiberRef = { current: void 0 };
86
+ const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid);
87
+ yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
88
+ return Effect.gen(function* () {
89
+ yield* Ref.set(stoppedRef, true);
90
+ yield* Scope.close(stateScopeRef.current, Exit.void);
91
+ yield* Scope.close(actorScope, Exit.void);
92
+ yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
93
+ return yield* Effect.failCause(cause);
94
+ });
95
+ }));
96
+ /** Set the exit deferred exactly once. */
97
+ const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
98
+ if (machine.finalStates.has(machine.initial._tag)) {
99
+ if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
100
+ yield* Ref.set(stoppedRef, true);
101
+ yield* Scope.close(stateScopeRef.current, Exit.void);
102
+ yield* Scope.close(actorScope, Exit.void);
103
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
104
+ yield* setExit(ActorExit.Final(machine.initial));
105
+ return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
106
+ }
107
+ const augmentedHooks = {
108
+ ...hooks,
109
+ onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid)
110
+ };
111
+ const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, lifecycle, config.wrapProcess));
112
+ loopFiberRef.current = loopFiber;
113
+ if (backgroundFibers.length > 0) yield* Effect.forkDaemon(Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
114
+ if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Fiber.interrupt(loopFiber)));
115
+ return Effect.never;
116
+ })))).pipe(Effect.catchAllCause(() => Effect.void)));
117
+ const stop = Effect.gen(function* () {
118
+ if (yield* Ref.get(stoppedRef)) return;
119
+ if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
120
+ yield* Ref.set(stoppedRef, true);
121
+ yield* Fiber.interrupt(loopFiber);
122
+ yield* Scope.close(stateScopeRef.current, Exit.void);
123
+ yield* Scope.close(actorScope, Exit.void);
124
+ yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
125
+ yield* setExit(ActorExit.Stopped);
126
+ }).pipe(Effect.asVoid);
127
+ if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
128
+ return {
129
+ ...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
130
+ stop
131
+ };
132
+ });
133
+ /**
134
+ * Build the runtime handle (send/ask/getState/isStopped).
135
+ * Shared between initial-final and normal paths.
136
+ */
137
+ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
138
+ send: (event) => Effect.gen(function* () {
139
+ if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
140
+ _tag: "send",
141
+ event
142
+ });
143
+ }),
144
+ sendWait: (event) => Effect.gen(function* () {
145
+ if (!(yield* Ref.get(stoppedRef))) {
146
+ const done = yield* Deferred.make();
147
+ yield* Queue.offer(eventQueue, {
148
+ _tag: "sendWait",
149
+ event,
150
+ done
151
+ });
152
+ yield* Deferred.await(done);
153
+ }
154
+ }),
155
+ ask: (event) => Effect.gen(function* () {
156
+ if (yield* Ref.get(stoppedRef)) return yield* new NoReplyError({
157
+ actorId: "stopped",
158
+ eventTag: event._tag
159
+ });
160
+ const reply = yield* Deferred.make();
161
+ yield* Queue.offer(eventQueue, {
162
+ _tag: "ask",
163
+ event,
164
+ reply
165
+ });
166
+ return yield* Deferred.await(reply);
167
+ }),
168
+ getState: SubscriptionRef.get(stateRef),
169
+ stateRef,
170
+ isStopped: Ref.get(stoppedRef),
171
+ stop: Effect.void,
172
+ _queue: eventQueue,
173
+ _stoppedRef: stoppedRef,
174
+ exitDeferred,
175
+ actorScope
176
+ });
177
+ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, lifecycle, wrapProcess) {
178
+ /** Set the exit deferred exactly once. */
179
+ const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
180
+ const postponed = [];
181
+ const hasPostponeRules = machine.postponeRules.length > 0;
182
+ const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
183
+ const event = queued.event;
184
+ const currentState = yield* SubscriptionRef.get(stateRef);
185
+ if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
186
+ if (queued._tag === "call") {
187
+ const postponedResult = {
188
+ newState: currentState,
189
+ previousState: currentState,
190
+ transitioned: false,
191
+ lifecycleRan: false,
192
+ isFinal: false,
193
+ hasReply: false,
194
+ deferReply: false,
195
+ reply: void 0,
196
+ postponed: true
197
+ };
198
+ yield* Deferred.succeed(queued.reply, postponedResult);
199
+ }
200
+ if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
201
+ postponed.push({
202
+ _tag: "send",
203
+ event
204
+ });
205
+ return {
206
+ shouldStop: false,
207
+ stateChanged: false,
208
+ result: {
209
+ newState: currentState,
210
+ previousState: currentState,
211
+ transitioned: false,
212
+ lifecycleRan: false,
213
+ isFinal: false,
214
+ hasReply: false,
215
+ deferReply: false,
216
+ reply: void 0,
217
+ postponed: true
218
+ }
219
+ };
220
+ }
221
+ if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
222
+ const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
223
+ if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
224
+ if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
225
+ switch (queued._tag) {
226
+ case "call":
227
+ yield* Deferred.succeed(queued.reply, result);
228
+ break;
229
+ case "sendWait":
230
+ yield* Deferred.succeed(queued.done, void 0);
231
+ break;
232
+ case "ask":
233
+ if (result.hasReply) {
234
+ const replySchema = machine._replySchemas?.get(event._tag);
235
+ if (replySchema !== void 0) {
236
+ let decoded;
237
+ try {
238
+ decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
239
+ } catch (decodeError) {
240
+ yield* Deferred.die(queued.reply, decodeError);
241
+ return yield* Effect.die(decodeError);
242
+ }
243
+ yield* Deferred.succeed(queued.reply, decoded);
244
+ } else yield* Deferred.succeed(queued.reply, result.reply);
245
+ } else yield* Deferred.fail(queued.reply, new NoReplyError({
246
+ actorId,
247
+ eventTag: event._tag
248
+ }));
249
+ break;
250
+ }
251
+ if (lifecycle?.onProcessed !== void 0 && result.transitioned) yield* lifecycle.onProcessed(result, event);
252
+ const shouldStop = result.isFinal && result.lifecycleRan;
253
+ if (shouldStop && lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(result.newState);
254
+ return {
255
+ shouldStop,
256
+ stateChanged: result.lifecycleRan,
257
+ result
258
+ };
259
+ });
260
+ const shutdown = (exitReason) => Effect.gen(function* () {
261
+ yield* Ref.set(stoppedRef, true);
262
+ if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
263
+ settlePostponed(postponed, actorId);
264
+ const remaining = yield* Queue.takeAll(eventQueue);
265
+ for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
266
+ else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
267
+ actorId,
268
+ eventTag: entry.event._tag
269
+ })));
270
+ else if (entry._tag === "call") {
271
+ const currentState = yield* SubscriptionRef.get(stateRef);
272
+ Effect.runFork(Deferred.succeed(entry.reply, {
273
+ newState: currentState,
274
+ previousState: currentState,
275
+ transitioned: false,
276
+ lifecycleRan: false,
277
+ isFinal: machine.finalStates.has(currentState._tag),
278
+ hasReply: false,
279
+ deferReply: false,
280
+ reply: void 0,
281
+ postponed: false
282
+ }));
283
+ }
284
+ yield* Scope.close(stateScopeRef.current, Exit.void);
285
+ yield* setExit(exitReason);
286
+ });
287
+ while (true) {
288
+ const queued = yield* Queue.take(eventQueue);
289
+ if (queued._tag === "drain") {
290
+ yield* shutdown(ActorExit.Stopped);
291
+ yield* Deferred.succeed(queued.done, void 0);
292
+ return;
293
+ }
294
+ const eventQueued = queued;
295
+ const processInner = processQueued(eventQueued);
296
+ const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
297
+ return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
298
+ }) : processInner).pipe(Effect.catchAllCause((cause) => {
299
+ if (queued._tag === "sendWait") Effect.runFork(Deferred.succeed(queued.done, void 0));
300
+ else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
301
+ else if (queued._tag === "call") Effect.runFork(Deferred.failCause(queued.reply, cause));
302
+ return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.zipRight(Effect.failCause(cause)));
303
+ }));
304
+ if (shouldStop) {
305
+ const finalState = yield* SubscriptionRef.get(stateRef);
306
+ yield* shutdown(ActorExit.Final(finalState));
307
+ return;
308
+ }
309
+ let drainTriggered = stateChanged;
310
+ while (drainTriggered && postponed.length > 0) {
311
+ drainTriggered = false;
312
+ const drained = postponed.splice(0);
313
+ for (const entry of drained) {
314
+ const drain = yield* processQueued(entry);
315
+ if (drain.shouldStop) {
316
+ const finalState = yield* SubscriptionRef.get(stateRef);
317
+ yield* shutdown(ActorExit.Final(finalState));
318
+ return;
319
+ }
320
+ if (drain.stateChanged) drainTriggered = true;
321
+ }
322
+ }
323
+ }
324
+ });
325
+ /** Settle all pending Deferreds in the postpone buffer on shutdown. */
326
+ const settlePostponed = (postponed, actorId) => {
327
+ for (const entry of postponed) if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
328
+ actorId,
329
+ eventTag: entry.event._tag
330
+ })));
331
+ else if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
332
+ postponed.length = 0;
333
+ };
334
+ //#endregion
335
+ export { createRuntime };
@@ -1,5 +1,5 @@
1
1
  import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
2
- import { BuiltMachine, Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
2
+ import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
3
3
  import { ActorSystem } from "../actor.js";
4
4
  import { Cause, Effect, Scope } from "effect";
5
5
 
@@ -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
  /**
@@ -66,6 +68,8 @@ interface ProcessEventHooks<S, E> {
66
68
  readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
67
69
  /** Called when a transition handler or spawn effect fails with a defect */
68
70
  readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
71
+ /** Called when a forked spawn fiber defects — signals the runtime to set exitDeferred */
72
+ readonly onSpawnDefect?: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
69
73
  }
70
74
  /**
71
75
  * Error info for inspection hooks.
@@ -92,6 +96,8 @@ interface ProcessEventResult<S> {
92
96
  readonly isFinal: boolean;
93
97
  /** Whether the handler provided a reply (structural, not value-based) */
94
98
  readonly hasReply: boolean;
99
+ /** Whether the handler deferred the reply to a spawn handler (Machine.deferReply) */
100
+ readonly deferReply: boolean;
95
101
  /** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
96
102
  readonly reply?: unknown;
97
103
  /** Whether the event was postponed (buffered for retry after next state change) */
@@ -131,6 +137,7 @@ declare const processEventCore: <S extends {
131
137
  lifecycleRan: boolean;
132
138
  isFinal: boolean;
133
139
  hasReply: boolean;
140
+ deferReply: boolean;
134
141
  reply: unknown;
135
142
  postponed: boolean;
136
143
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
@@ -143,7 +150,7 @@ declare const runSpawnEffects: <S extends {
143
150
  readonly _tag: string;
144
151
  }, E extends {
145
152
  readonly _tag: string;
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>>;
153
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined, onSpawnDefect?: ((cause: Cause.Cause<unknown>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
147
154
  /**
148
155
  * Resolve which transition should fire for a given state and event.
149
156
  * Uses indexed O(1) lookup. First matching transition wins.
@@ -161,14 +168,13 @@ declare const invalidateIndex: (machine: object) => void;
161
168
  * Find all transitions matching a state/event pair.
162
169
  * Returns empty array if no matches.
163
170
  *
164
- * Accepts both `Machine` and `BuiltMachine`.
165
171
  * O(1) lookup after first access (index is lazily built).
166
172
  */
167
173
  declare const findTransitions: <S extends {
168
174
  readonly _tag: string;
169
175
  }, E extends {
170
176
  readonly _tag: string;
171
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, GD, EFD, R>>;
177
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: Machine<S, E, R, any, any, GD, EFD>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, GD, EFD, R>>;
172
178
  /**
173
179
  * Find all spawn effects for a state.
174
180
  * Returns empty array if no matches.