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/dist/schema.js CHANGED
@@ -38,6 +38,7 @@ import { Schema } from "effect";
38
38
  *
39
39
  * @module
40
40
  */
41
+ const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
41
42
  /**
42
43
  * Build a schema-first definition from a record of tag -> fields
43
44
  */
@@ -45,9 +46,14 @@ const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
45
46
  const buildMachineSchema = (definition) => {
46
47
  const variants = {};
47
48
  const constructors = {};
49
+ const replySchemas = /* @__PURE__ */ new Map();
48
50
  for (const tag of Object.keys(definition)) {
49
51
  const fields = definition[tag];
50
52
  if (fields === void 0) continue;
53
+ if (ReplySchemaSymbol in fields) {
54
+ const rs = fields[ReplySchemaSymbol];
55
+ if (rs !== void 0) replySchemas.set(tag, rs);
56
+ }
51
57
  variants[tag] = Schema.TaggedStruct(tag, fields);
52
58
  const fieldNames = new Set(Object.keys(fields));
53
59
  if (fieldNames.size > 0) {
@@ -72,7 +78,7 @@ const buildMachineSchema = (definition) => {
72
78
  };
73
79
  }
74
80
  const variantArray = Object.values(variants);
75
- if (variantArray.length === 0) throw new InvalidSchemaError({});
81
+ if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
76
82
  const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
77
83
  const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
78
84
  const $match = (valueOrCases, maybeCases) => {
@@ -94,6 +100,7 @@ const buildMachineSchema = (definition) => {
94
100
  variants,
95
101
  constructors,
96
102
  _definition: definition,
103
+ replySchemas,
97
104
  $is,
98
105
  $match
99
106
  };
@@ -103,10 +110,11 @@ const buildMachineSchema = (definition) => {
103
110
  * Builds the schema object with variants, constructors, $is, and $match.
104
111
  */
105
112
  const createMachineSchema = (definition) => {
106
- const { schema, variants, constructors, _definition, $is, $match } = buildMachineSchema(definition);
113
+ const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
107
114
  return Object.assign(Object.create(schema), {
108
115
  variants,
109
116
  _definition,
117
+ _replySchemas: replySchemas,
110
118
  $is,
111
119
  $match,
112
120
  ...constructors
@@ -149,19 +157,40 @@ const State = (definition) => createMachineSchema(definition);
149
157
  * accidental use of constructors from different event schemas
150
158
  * (unless they have identical definitions).
151
159
  *
160
+ * Use `Event.reply(fields, replySchema)` to define events that support
161
+ * typed `ask()` replies.
162
+ *
152
163
  * @example
153
164
  * ```ts
154
- * const OrderEvent = MachineSchema.Event({
165
+ * const OrderEvent = Event({
155
166
  * Ship: { trackingId: Schema.String },
156
167
  * Cancel: {},
168
+ * GetTotal: Event.reply({}, Schema.Number),
157
169
  * })
158
170
  *
159
171
  * type OrderEvent = typeof OrderEvent.Type
160
172
  *
161
173
  * // Construct
162
174
  * const e = OrderEvent.Ship({ trackingId: "abc" })
175
+ *
176
+ * // Typed ask
177
+ * const total = yield* actor.ask(OrderEvent.GetTotal) // number
163
178
  * ```
164
179
  */
165
- const Event = (definition) => createMachineSchema(definition);
180
+ const EventImpl = (definition) => createMachineSchema(definition);
181
+ /**
182
+ * Annotate event fields with a reply schema.
183
+ * Events defined with `Event.reply(fields, replySchema)` enable typed `ask()`.
184
+ */
185
+ const replyFieldsFn = (fields, replySchema) => {
186
+ const annotated = { ...fields };
187
+ Object.defineProperty(annotated, ReplySchemaSymbol, {
188
+ value: replySchema,
189
+ enumerable: false,
190
+ writable: false
191
+ });
192
+ return annotated;
193
+ };
194
+ const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
166
195
  //#endregion
167
196
  export { Event, State };
@@ -0,0 +1,97 @@
1
+ import { Cause, Duration, Schedule } from "effect";
2
+
3
+ //#region src/supervision.d.ts
4
+ /**
5
+ * Where in the actor lifecycle a defect occurred.
6
+ *
7
+ * - `transition` — during event handler execution
8
+ * - `spawn` — during state spawn effect execution
9
+ * - `background` — in a background effect fiber
10
+ * - `initial-spawn` — during initial state spawn effects (before event loop)
11
+ */
12
+ type DefectPhase = "transition" | "spawn" | "background" | "initial-spawn";
13
+ /**
14
+ * Terminal exit reason for an actor generation.
15
+ *
16
+ * - `Final` — machine reached a final state normally
17
+ * - `Stopped` — explicit `actor.stop` or `actor.drain`
18
+ * - `Defect` — unhandled error in the runtime
19
+ */
20
+ type ActorExit<S> = {
21
+ readonly _tag: "Final";
22
+ readonly state: S;
23
+ } | {
24
+ readonly _tag: "Stopped";
25
+ } | {
26
+ readonly _tag: "Defect";
27
+ readonly cause: Cause.Cause<unknown>;
28
+ readonly phase: DefectPhase;
29
+ };
30
+ /** Constructors for ActorExit */
31
+ declare const ActorExit: {
32
+ readonly Final: <S>(state: S) => ActorExit<S>;
33
+ readonly Stopped: ActorExit<never>;
34
+ readonly Defect: <S = never>(cause: Cause.Cause<unknown>, phase: DefectPhase) => ActorExit<S>;
35
+ };
36
+ /**
37
+ * Phase state for supervised actors. Serializes concurrent stop/restart/drain.
38
+ *
39
+ * Transitions:
40
+ * - `Running` → crash → `Restarting` → new runtime → `Running`
41
+ * - `Running` → explicit stop/drain → `Stopping` → `Terminated`
42
+ * - `Restarting` → explicit stop → `Stopping` → `Terminated`
43
+ *
44
+ * @internal
45
+ */
46
+ type CellPhase<S> = {
47
+ readonly _tag: "Running";
48
+ readonly generation: number;
49
+ } | {
50
+ readonly _tag: "Restarting";
51
+ readonly generation: number;
52
+ } | {
53
+ readonly _tag: "Stopping";
54
+ } | {
55
+ readonly _tag: "Terminated";
56
+ readonly exit: ActorExit<S>;
57
+ };
58
+ declare namespace Supervision {
59
+ /**
60
+ * Supervision policy for actor restart behavior.
61
+ *
62
+ * `schedule` controls restart timing and budget — schedule exhaustion means terminal stop.
63
+ * `shouldRestart` optionally classifies defects — return `false` to stop immediately
64
+ * without consuming the schedule.
65
+ */
66
+ interface Policy {
67
+ /** Schedule that controls restart timing. Exhaustion = terminal stop. */
68
+ readonly schedule: Schedule.Schedule<unknown>;
69
+ /**
70
+ * Optional classifier: given a defect exit, decide whether to restart or stop immediately.
71
+ * Default: always restart (let schedule handle budget).
72
+ */
73
+ readonly shouldRestart?: (exit: Extract<ActorExit<unknown>, {
74
+ readonly _tag: "Defect";
75
+ }>) => boolean;
76
+ }
77
+ /** No supervision — crashes are terminal. */
78
+ const none: Policy;
79
+ /**
80
+ * Restart on defect with max restarts within a window, optional backoff.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * Supervision.restart() // unlimited restarts, no backoff
85
+ * Supervision.restart({ maxRestarts: 3 }) // 3 restarts then terminal
86
+ * Supervision.restart({ maxRestarts: 3, within: "1 minute" }) // 3 within 1 min
87
+ * Supervision.restart({ backoff: Schedule.exponential("100 millis") })
88
+ * ```
89
+ */
90
+ const restart: (options?: {
91
+ readonly maxRestarts?: number;
92
+ readonly within?: Duration.Input;
93
+ readonly backoff?: Schedule.Schedule<unknown>;
94
+ }) => Policy;
95
+ }
96
+ //#endregion
97
+ export { ActorExit, CellPhase, DefectPhase, Supervision };
@@ -0,0 +1,42 @@
1
+ import { Schedule } from "effect";
2
+ //#region src/supervision.ts
3
+ /**
4
+ * Supervision types for actor lifecycle management.
5
+ *
6
+ * Core concepts:
7
+ * - `ActorExit<S>` — why an actor stopped (final, explicit stop, or defect)
8
+ * - `DefectPhase` — where in the lifecycle a defect occurred
9
+ * - `Supervision.Policy` — Schedule-based restart policy
10
+ * - `CellPhase<S>` — internal phase machine for serializing stop/restart/drain
11
+ *
12
+ * @module
13
+ */
14
+ /** Constructors for ActorExit */
15
+ const ActorExit = {
16
+ Final: (state) => ({
17
+ _tag: "Final",
18
+ state
19
+ }),
20
+ Stopped: { _tag: "Stopped" },
21
+ Defect: (cause, phase) => ({
22
+ _tag: "Defect",
23
+ cause,
24
+ phase
25
+ })
26
+ };
27
+ let Supervision;
28
+ (function(_Supervision) {
29
+ _Supervision.none = { schedule: Schedule.recurs(0) };
30
+ _Supervision.restart = (options) => {
31
+ let schedule = Schedule.forever;
32
+ if (options?.maxRestarts !== void 0) {
33
+ const recurs = Schedule.recurs(options.maxRestarts);
34
+ if (options.within !== void 0) schedule = Schedule.both(recurs, Schedule.windowed(options.within));
35
+ else schedule = recurs;
36
+ }
37
+ if (options?.backoff !== void 0) schedule = Schedule.both(schedule, options.backoff);
38
+ return { schedule };
39
+ };
40
+ })(Supervision || (Supervision = {}));
41
+ //#endregion
42
+ export { ActorExit, Supervision };
package/dist/testing.d.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
2
1
  import { AssertionError } from "./errors.js";
3
- import { BuiltMachine, Machine, MachineRef } from "./machine.js";
2
+ import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
3
+ import { Machine, MachineRef } from "./machine.js";
4
4
  import { Effect, SubscriptionRef } from "effect";
5
5
 
6
6
  //#region src/testing.d.ts
7
- /** Accept either Machine or BuiltMachine for testing utilities. */
8
- type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD> | BuiltMachine<S, E, R>;
7
+ type MachineInput<S, E, R, GD extends GuardsDef, EFD extends EffectsDef> = Machine<S, E, R, any, any, GD, EFD>;
9
8
  /**
10
9
  * Result of simulating events through a machine
11
10
  */
@@ -37,7 +36,9 @@ declare const simulate: <S extends {
37
36
  readonly _tag: string;
38
37
  }, E extends {
39
38
  readonly _tag: string;
40
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[]) => Effect.Effect<{
39
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], options?: {
40
+ slots?: Record<string, any>;
41
+ } | undefined) => Effect.Effect<{
41
42
  states: S[];
42
43
  finalState: S;
43
44
  }, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
@@ -48,7 +49,9 @@ declare const assertReaches: <S extends {
48
49
  readonly _tag: string;
49
50
  }, E extends {
50
51
  readonly _tag: string;
51
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedTag: string, options?: {
53
+ slots?: Record<string, any>;
54
+ } | undefined) => Effect.Effect<S, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
52
55
  /**
53
56
  * Assert that a machine follows a specific path of state tags
54
57
  *
@@ -65,7 +68,9 @@ declare const assertPath: <S extends {
65
68
  readonly _tag: string;
66
69
  }, E extends {
67
70
  readonly _tag: string;
68
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
71
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], expectedPath: readonly string[], options?: {
72
+ slots?: Record<string, any>;
73
+ } | undefined) => Effect.Effect<{
69
74
  states: S[];
70
75
  finalState: S;
71
76
  }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
@@ -86,7 +91,9 @@ declare const assertNeverReaches: <S extends {
86
91
  readonly _tag: string;
87
92
  }, E extends {
88
93
  readonly _tag: string;
89
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
94
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string, options?: {
95
+ slots?: Record<string, any>;
96
+ } | undefined) => Effect.Effect<{
90
97
  states: S[];
91
98
  finalState: S;
92
99
  }, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
@@ -107,6 +114,8 @@ interface TestHarnessOptions<S, E> {
107
114
  * Useful for logging or spying on transitions.
108
115
  */
109
116
  readonly onTransition?: (from: S, event: E, to: S) => void;
117
+ /** Slot handler implementations for machines with guards/effects. */
118
+ readonly slots?: Record<string, any>;
110
119
  }
111
120
  /**
112
121
  * Create a test harness for step-by-step testing.
package/dist/testing.js CHANGED
@@ -1,9 +1,18 @@
1
1
  import { stubSystem } from "./internal/utils.js";
2
- import { AssertionError } from "./errors.js";
3
- import { BuiltMachine } from "./machine.js";
4
2
  import { executeTransition, shouldPostpone } from "./internal/transition.js";
3
+ import { AssertionError } from "./errors.js";
4
+ import { materializeMachine } from "./machine.js";
5
5
  import { Effect, SubscriptionRef } from "effect";
6
6
  //#region src/testing.ts
7
+ const makeDummySelf = (label) => {
8
+ const dummySend = Effect.fn(label)((_event) => Effect.void);
9
+ return {
10
+ send: dummySend,
11
+ cast: dummySend,
12
+ spawn: () => Effect.die(`spawn not supported in ${label}`),
13
+ reply: () => Effect.succeed(false)
14
+ };
15
+ };
7
16
  /**
8
17
  * Simulate a sequence of events through a machine without running an actor.
9
18
  * Useful for testing state transitions in isolation.
@@ -24,14 +33,9 @@ import { Effect, SubscriptionRef } from "effect";
24
33
  * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
25
34
  * ```
26
35
  */
27
- const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
28
- const machine = input instanceof BuiltMachine ? input._inner : input;
29
- const dummySend = Effect.fn("effect-machine.testing.simulate.send")((_event) => Effect.void);
30
- const dummySelf = {
31
- send: dummySend,
32
- cast: dummySend,
33
- spawn: () => Effect.die("spawn not supported in simulation")
34
- };
36
+ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events, options) {
37
+ const machine = materializeMachine(input, options?.slots);
38
+ const dummySelf = makeDummySelf("effect-machine.testing.simulate");
35
39
  let currentState = machine.initial;
36
40
  const states = [currentState];
37
41
  const hasPostponeRules = machine.postponeRules.length > 0;
@@ -73,8 +77,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
73
77
  /**
74
78
  * Assert that a machine can reach a specific state given a sequence of events
75
79
  */
76
- const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
77
- const result = yield* simulate(input, events);
80
+ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag, options) {
81
+ const result = yield* simulate(input, events, options);
78
82
  if (result.finalState._tag !== expectedTag) return yield* new AssertionError({ message: `Expected final state "${expectedTag}" but got "${result.finalState._tag}". States visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
79
83
  return result.finalState;
80
84
  });
@@ -90,8 +94,8 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
90
94
  * )
91
95
  * ```
92
96
  */
93
- const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
94
- const result = yield* simulate(input, events);
97
+ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath, options) {
98
+ const result = yield* simulate(input, events, options);
95
99
  const actualPath = result.states.map((s) => s._tag);
96
100
  if (actualPath.length !== expectedPath.length) return yield* new AssertionError({ message: `Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
97
101
  for (let i = 0; i < expectedPath.length; i++) if (actualPath[i] !== expectedPath[i]) return yield* new AssertionError({ message: `Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
@@ -110,8 +114,8 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
110
114
  * )
111
115
  * ```
112
116
  */
113
- const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
114
- const result = yield* simulate(input, events);
117
+ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag, options) {
118
+ const result = yield* simulate(input, events, options);
115
119
  const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
116
120
  if (visitedIndex !== -1) return yield* new AssertionError({ message: `Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\nStates visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
117
121
  return result;
@@ -138,13 +142,8 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
138
142
  * ```
139
143
  */
140
144
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
141
- const machine = input instanceof BuiltMachine ? input._inner : input;
142
- const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
143
- const dummySelf = {
144
- send: dummySend,
145
- cast: dummySend,
146
- spawn: () => Effect.die("spawn not supported in test harness")
147
- };
145
+ const machine = materializeMachine(input, options?.slots);
146
+ const dummySelf = makeDummySelf("effect-machine.testing.harness");
148
147
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
148
  const hasPostponeRules = machine.postponeRules.length > 0;
150
149
  const postponed = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
@@ -56,20 +56,20 @@
56
56
  "release": "bun run build && changeset publish"
57
57
  },
58
58
  "dependencies": {
59
- "effect": "4.0.0-beta.35"
59
+ "effect": "4.0.0-beta.42"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@changesets/changelog-github": "^0.6.0",
63
63
  "@changesets/cli": "^2.30.0",
64
- "@effect/language-service": "^0.82.0",
64
+ "@effect/language-service": "^0.84.2",
65
65
  "@types/bun": "1.3.11",
66
66
  "concurrently": "^9.2.1",
67
67
  "effect-bun-test": "0.3.0",
68
68
  "effect-v3": "npm:effect@^3.21.0",
69
69
  "lefthook": "^2.1.4",
70
- "oxfmt": "^0.41.0",
71
- "oxlint": "^1.56.0",
72
- "tsdown": "^0.21.4",
70
+ "oxfmt": "^0.42.0",
71
+ "oxlint": "^1.57.0",
72
+ "tsdown": "^0.21.7",
73
73
  "typescript": "^5.9.3"
74
74
  },
75
75
  "peerDependencies": {
@@ -81,6 +81,6 @@
81
81
  }
82
82
  },
83
83
  "overrides": {
84
- "effect": "4.0.0-beta.35"
84
+ "effect": "4.0.0-beta.42"
85
85
  }
86
86
  }
@@ -1,29 +1,15 @@
1
- import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
1
+ import { ActorExit, Supervision } from "./supervision.js";
2
+ import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
2
3
  import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
4
+ import { EffectsDef, GuardsDef } from "./slot.js";
3
5
  import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
4
- import { BuiltMachine, Machine, MachineRef } from "./machine.js";
6
+ import { Machine } from "./machine.js";
7
+ import { RuntimeQueuedEvent } from "./internal/runtime.js";
5
8
  import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
6
- import * as effect_dist_dts_Tracer_js0 from "effect/dist/dts/Tracer.js";
7
9
 
8
10
  //#region src/actor.d.ts
9
- /** Discriminated mailbox request */
10
- type QueuedEvent<E> = {
11
- readonly _tag: "send";
12
- readonly event: E;
13
- } | {
14
- readonly _tag: "call";
15
- readonly event: E;
16
- readonly reply: Deferred.Deferred<ProcessEventResult<{
17
- readonly _tag: string;
18
- }>, ActorStoppedError>;
19
- } | {
20
- readonly _tag: "ask";
21
- readonly event: E;
22
- readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
23
- };
24
- /**
25
- * Reference to a running actor.
26
- */
11
+ /** Discriminated mailbox request — alias for RuntimeQueuedEvent */
12
+ type QueuedEvent<E> = RuntimeQueuedEvent<E>;
27
13
  /**
28
14
  * Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
29
15
  */
@@ -59,11 +45,11 @@ interface ActorRef<State extends {
59
45
  */
60
46
  readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
61
47
  /**
62
- * Typed request-reply. Event is processed through the queue; caller gets
63
- * the domain value returned by the handler's `reply` field.
48
+ * Typed request-reply. Accepts only events with a reply schema
49
+ * (defined via `Event.reply()`). Return type is inferred from the schema.
64
50
  * Fails with NoReplyError if the handler doesn't provide a reply.
65
51
  */
66
- readonly ask: <R>(event: Event) => Effect.Effect<R, NoReplyError | ActorStoppedError>;
52
+ readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError | ActorStoppedError>;
67
53
  /** Observable state. */
68
54
  readonly state: SubscriptionRef.SubscriptionRef<State>;
69
55
  /** Stop the actor gracefully. */
@@ -103,6 +89,25 @@ interface ActorRef<State extends {
103
89
  };
104
90
  /** Subscribe to state changes (sync callback). Returns unsubscribe function. */
105
91
  readonly subscribe: (fn: (state: State) => void) => () => void;
92
+ /**
93
+ * Wait for this actor's terminal exit. Resolves with the exit reason.
94
+ * Set exactly once when the actor terminates (final, stop, drain, or defect).
95
+ */
96
+ readonly awaitExit: Effect.Effect<ActorExit<State>>;
97
+ /**
98
+ * Watch another actor. Returns an Effect that resolves with the exit reason
99
+ * when the watched actor terminally stops. Ignores restarts.
100
+ * Built on the other actor's exitDeferred — authoritative, not system events.
101
+ */
102
+ readonly watch: (other: {
103
+ readonly id: string;
104
+ readonly awaitExit: Effect.Effect<ActorExit<unknown>>;
105
+ }) => Effect.Effect<ActorExit<unknown>>;
106
+ /**
107
+ * Drain: process all remaining events in the queue, then stop.
108
+ * Unlike `stop` (which interrupts immediately), `drain` lets the actor finish its work.
109
+ */
110
+ readonly drain: Effect.Effect<void>;
106
111
  /** Sync helpers for non-Effect boundaries. */
107
112
  readonly sync: ActorRefSync<State, Event>;
108
113
  /** The actor system this actor belongs to. */
@@ -121,10 +126,17 @@ type SystemEvent = {
121
126
  readonly _tag: "ActorSpawned";
122
127
  readonly id: string;
123
128
  readonly actor: ActorRef<AnyState, unknown>;
129
+ } | {
130
+ readonly _tag: "ActorRestarted";
131
+ readonly id: string;
132
+ readonly actor: ActorRef<AnyState, unknown>;
133
+ readonly generation: number;
134
+ readonly exit: ActorExit<unknown>;
124
135
  } | {
125
136
  readonly _tag: "ActorStopped";
126
137
  readonly id: string;
127
138
  readonly actor: ActorRef<AnyState, unknown>;
139
+ readonly exit: ActorExit<unknown>;
128
140
  };
129
141
  /**
130
142
  * Listener callback for system events.
@@ -136,18 +148,15 @@ type SystemEventListener = (event: SystemEvent) => void;
136
148
  interface ActorSystem {
137
149
  /**
138
150
  * Spawn a new actor with the given machine.
139
- *
140
- * @example
141
- * ```ts
142
- * const built = machine.build({ fetchData: ... })
143
- * const actor = yield* system.spawn("my-actor", built);
144
- * ```
145
151
  */
146
152
  readonly spawn: <S extends {
147
153
  readonly _tag: string;
148
154
  }, E extends {
149
155
  readonly _tag: string;
150
- }, R>(id: string, machine: BuiltMachine<S, E, R>) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
156
+ }, R>(id: string, machine: Machine<S, E, R, any, any, any, any>, options?: {
157
+ slots?: Record<string, any>;
158
+ supervision?: Supervision.Policy;
159
+ }) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
151
160
  /**
152
161
  * Get an existing actor by ID
153
162
  */
@@ -183,15 +192,16 @@ type Listeners<S> = Set<(state: S) => void>;
183
192
  */
184
193
  declare const notifyListeners: <S>(listeners: Listeners<S>, state: S) => void;
185
194
  /**
186
- * Build core ActorRef methods shared between regular and persistent actors.
195
+ * Build core ActorRef methods.
187
196
  */
188
197
  declare const buildActorRefCore: <S extends {
189
198
  readonly _tag: string;
190
199
  }, E extends {
191
200
  readonly _tag: string;
192
- }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueue: Queue.Queue<QueuedEvent<E>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub?: PubSub.PubSub<TransitionInfo<S, E>>) => ActorRef<S, E>;
201
+ }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
193
202
  /**
194
- * Create and start an actor for a machine
203
+ * Create and start an actor for a machine.
204
+ * Uses the shared runtime kernel with lifecycle hooks for actor-specific concerns.
195
205
  */
196
206
  declare const createActor: <S extends {
197
207
  readonly _tag: string;
@@ -199,12 +209,19 @@ declare const createActor: <S extends {
199
209
  readonly _tag: string;
200
210
  }, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: {
201
211
  initialState?: S;
202
- } | undefined) => Effect.Effect<ActorRef<S, E>, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, effect_dist_dts_Tracer_js0.ParentSpan> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope> | Exclude<Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>, effect_dist_dts_Tracer_js0.ParentSpan>>;
212
+ supervision?: Supervision.Policy; /** @internal Called by system after each restart emits ActorRestarted system event */
213
+ onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
214
+ } | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
203
215
  /** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
204
216
  declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
217
+ /**
218
+ * Create an ActorSystem instance. Must be run in a Scope.
219
+ * @internal — use Default layer for normal usage
220
+ */
221
+ declare const makeSystem: () => Effect.Effect<ActorSystem, never, Scope.Scope>;
205
222
  /**
206
223
  * Default ActorSystem layer
207
224
  */
208
- declare const Default: Layer.Layer<ActorSystem, never, never>;
225
+ declare const Default: Layer.Layer<ActorSystem, never, Scope.Scope>;
209
226
  //#endregion
210
- export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
227
+ export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };