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
@@ -1,7 +1,17 @@
1
- import { FullEventBrand, FullStateBrand } from "./internal/brands.js";
1
+ import { FullEventBrand, FullStateBrand, ReplyTypeBrand } from "./internal/brands.js";
2
2
  import { Schema } from "effect";
3
3
 
4
4
  //#region src/schema.d.ts
5
+ declare const ReplySchemaSymbol: unique symbol;
6
+ type ReplySchemaSymbol = typeof ReplySchemaSymbol;
7
+ /**
8
+ * Fields annotated with a reply schema.
9
+ * Structurally identical to Schema.Struct.Fields at runtime,
10
+ * but carries the reply schema type at compile time.
11
+ */
12
+ type ReplyFields<F extends Schema.Struct.Fields, RS extends Schema.Schema.Any> = F & {
13
+ readonly [ReplySchemaSymbol]: RS;
14
+ };
5
15
  /**
6
16
  * Extract the TypeScript type from a TaggedStruct schema
7
17
  */
@@ -12,20 +22,23 @@ type TaggedStructType<Tag extends string, Fields extends Schema.Struct.Fields> =
12
22
  type VariantSchemas<D extends Record<string, Schema.Struct.Fields>> = { readonly [K in keyof D & string]: Schema.TaggedStruct<K, D[K]> };
13
23
  /**
14
24
  * Build union type from variant schemas.
15
- * Used for constraining fluent method type params.
25
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() inference.
16
26
  */
17
- type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> }[keyof D & string];
27
+ type VariantsUnion<D extends Record<string, Schema.Struct.Fields>> = { [K in keyof D & string]: TaggedStructType<K, D[K]> & (D[K] extends {
28
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R, infer _I, infer _RR>;
29
+ } ? ReplyTypeBrand<R> : unknown) }[keyof D & string];
18
30
  /**
19
- * Check if fields are empty (no required properties)
31
+ * Check if fields are empty (no required string properties).
32
+ * Symbol keys (like ReplySchemaSymbol) are metadata, not payload fields.
20
33
  */
21
- type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends never ? true : false;
34
+ type IsEmptyFields<Fields extends Schema.Struct.Fields> = string & keyof Fields extends never ? true : false;
22
35
  /**
23
- * Constructor functions for each variant.
24
- * Empty structs: plain values with `_tag`: `State.Idle`
25
- * Non-empty structs require args: `State.Loading({ url })`
26
- *
27
- * Each variant also has a `derive` method for constructing from a source object.
36
+ * Resolve the reply brand for a variant's fields.
37
+ * If fields carry ReplySchemaSymbol, adds ReplyTypeBrand<R>.
28
38
  */
39
+ type VariantReplyBrand<Fields extends Schema.Struct.Fields> = Fields extends {
40
+ readonly [ReplySchemaSymbol]: Schema.Schema<infer R, infer _I, infer _RR>;
41
+ } ? ReplyTypeBrand<R> : unknown;
29
42
  /**
30
43
  * Constructor functions for each variant.
31
44
  * Empty structs: plain values with `_tag`: `State.Idle`
@@ -33,10 +46,11 @@ type IsEmptyFields<Fields extends Schema.Struct.Fields> = keyof Fields extends n
33
46
  *
34
47
  * Each variant also has a `derive` method for constructing from a source object.
35
48
  * The source type uses `object` to accept branded state types without index signature issues.
49
+ * Reply-bearing variants carry ReplyTypeBrand<R> for ask() type inference.
36
50
  */
37
- type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & {
51
+ type VariantConstructors<D extends Record<string, Schema.Struct.Fields>, Brand> = { readonly [K in keyof D & string]: IsEmptyFields<D[K]> extends true ? TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]> & {
38
52
  readonly derive: (source: object) => TaggedStructType<K, D[K]> & Brand;
39
- } : ((args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand) & {
53
+ } : ((args: Schema.Struct.Constructor<D[K]>) => TaggedStructType<K, D[K]> & Brand & VariantReplyBrand<D[K]>) & {
40
54
  readonly derive: (source: object, partial?: Partial<Schema.Struct.Constructor<D[K]>>) => TaggedStructType<K, D[K]> & Brand;
41
55
  readonly _tag: K;
42
56
  } };
@@ -56,6 +70,11 @@ interface MachineSchemaBase<D extends Record<string, Schema.Struct.Fields>, Bran
56
70
  * Per-variant schemas for fine-grained operations
57
71
  */
58
72
  readonly variants: VariantSchemas<D>;
73
+ /**
74
+ * Reply schemas per variant tag. Only populated for event schemas
75
+ * with variants defined via `Event.reply()`.
76
+ */
77
+ readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
59
78
  /**
60
79
  * Type guard: `OrderState.$is("Pending")(value)`
61
80
  */
@@ -116,26 +135,8 @@ type MachineEventSchema<D extends Record<string, Schema.Struct.Fields>> = Schema
116
135
  * ```
117
136
  */
118
137
  declare const State: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineStateSchema<D>;
119
- /**
120
- * Create a schema-first Event definition.
121
- *
122
- * The schema's definition type D creates a unique brand, preventing
123
- * accidental use of constructors from different event schemas
124
- * (unless they have identical definitions).
125
- *
126
- * @example
127
- * ```ts
128
- * const OrderEvent = MachineSchema.Event({
129
- * Ship: { trackingId: Schema.String },
130
- * Cancel: {},
131
- * })
132
- *
133
- * type OrderEvent = typeof OrderEvent.Type
134
- *
135
- * // Construct
136
- * const e = OrderEvent.Ship({ trackingId: "abc" })
137
- * ```
138
- */
139
- declare const Event: <const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>;
138
+ declare const Event: (<const D extends Record<string, Schema.Struct.Fields>>(definition: D) => MachineEventSchema<D>) & {
139
+ reply: <F extends Schema.Struct.Fields, RS extends Schema.Schema.Any>(fields: F, replySchema: RS) => ReplyFields<F, RS>;
140
+ };
140
141
  //#endregion
141
- export { Event, MachineEventSchema, MachineStateSchema, State, VariantsUnion };
142
+ export { Event, MachineEventSchema, MachineStateSchema, ReplyFields, ReplySchemaSymbol, State, VariantsUnion };
package/v3/dist/schema.js CHANGED
@@ -38,15 +38,21 @@ 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
  */
44
45
  const buildMachineSchema = (definition) => {
45
46
  const variants = {};
46
47
  const constructors = {};
48
+ const replySchemas = /* @__PURE__ */ new Map();
47
49
  for (const tag of Object.keys(definition)) {
48
50
  const fields = definition[tag];
49
51
  if (fields === void 0) continue;
52
+ if (ReplySchemaSymbol in fields) {
53
+ const rs = fields[ReplySchemaSymbol];
54
+ if (rs !== void 0) replySchemas.set(tag, rs);
55
+ }
50
56
  variants[tag] = Schema.TaggedStruct(tag, fields);
51
57
  const fieldNames = new Set(Object.keys(fields));
52
58
  if (fieldNames.size > 0) {
@@ -93,6 +99,7 @@ const buildMachineSchema = (definition) => {
93
99
  variants,
94
100
  constructors,
95
101
  _definition: definition,
102
+ _replySchemas: replySchemas,
96
103
  $is,
97
104
  $match
98
105
  };
@@ -102,10 +109,11 @@ const buildMachineSchema = (definition) => {
102
109
  * Builds the schema object with variants, constructors, $is, and $match.
103
110
  */
104
111
  const createMachineSchema = (definition) => {
105
- const { schema, variants, constructors, _definition, $is, $match } = buildMachineSchema(definition);
112
+ const { schema, variants, constructors, _definition, _replySchemas, $is, $match } = buildMachineSchema(definition);
106
113
  return Object.assign(Object.create(schema), {
107
114
  variants,
108
115
  _definition,
116
+ _replySchemas,
109
117
  $is,
110
118
  $match,
111
119
  ...constructors
@@ -148,11 +156,15 @@ const State = (definition) => createMachineSchema(definition);
148
156
  * accidental use of constructors from different event schemas
149
157
  * (unless they have identical definitions).
150
158
  *
159
+ * Use `Event.reply(fields, replySchema)` to define events that support
160
+ * typed `ask()` replies.
161
+ *
151
162
  * @example
152
163
  * ```ts
153
- * const OrderEvent = MachineSchema.Event({
164
+ * const OrderEvent = Event({
154
165
  * Ship: { trackingId: Schema.String },
155
166
  * Cancel: {},
167
+ * GetTotal: Event.reply({}, Schema.Number),
156
168
  * })
157
169
  *
158
170
  * type OrderEvent = typeof OrderEvent.Type
@@ -161,6 +173,20 @@ const State = (definition) => createMachineSchema(definition);
161
173
  * const e = OrderEvent.Ship({ trackingId: "abc" })
162
174
  * ```
163
175
  */
164
- const Event = (definition) => createMachineSchema(definition);
176
+ const EventImpl = (definition) => createMachineSchema(definition);
177
+ /**
178
+ * Annotate event fields with a reply schema.
179
+ * Events defined with `Event.reply(fields, replySchema)` enable typed `ask()`.
180
+ */
181
+ const replyFieldsFn = (fields, replySchema) => {
182
+ const annotated = { ...fields };
183
+ Object.defineProperty(annotated, ReplySchemaSymbol, {
184
+ value: replySchema,
185
+ enumerable: false,
186
+ writable: false
187
+ });
188
+ return annotated;
189
+ };
190
+ const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
165
191
  //#endregion
166
192
  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.DurationInput;
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.intersect(recurs, Schedule.windowed(options.within));
35
+ else schedule = recurs;
36
+ }
37
+ if (options?.backoff !== void 0) schedule = Schedule.intersect(schedule, options.backoff);
38
+ return { schedule };
39
+ };
40
+ })(Supervision || (Supervision = {}));
41
+ //#endregion
42
+ export { ActorExit, Supervision };
@@ -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>>>>;
@@ -133,7 +140,9 @@ declare const createTestHarness: <S extends {
133
140
  readonly _tag: string;
134
141
  }, E extends {
135
142
  readonly _tag: string;
136
- }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
143
+ }, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, options?: (TestHarnessOptions<S, E> & {
144
+ slots?: Record<string, any>;
145
+ }) | undefined) => Effect.Effect<{
137
146
  state: SubscriptionRef.SubscriptionRef<S>;
138
147
  send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
139
148
  getState: Effect.Effect<S, never, never>;
@@ -1,9 +1,18 @@
1
1
  import { stubSystem } from "./internal/utils.js";
2
2
  import { AssertionError } from "./errors.js";
3
- import { BuiltMachine } from "./machine.js";
4
3
  import { executeTransition, shouldPostpone } from "./internal/transition.js";
4
+ import { materializeMachine } from "./machine.js";
5
5
  import { Effect, SubscriptionRef } from "effect";
6
6
  //#region src/testing.ts
7
+ /** Create a dummy MachineRef for testing utilities (no real send/spawn). */
8
+ const makeDummySelf = (label) => {
9
+ const dummySend = Effect.fn(`effect-machine.testing.${label}.send`)((_event) => Effect.void);
10
+ return {
11
+ send: dummySend,
12
+ cast: dummySend,
13
+ spawn: () => Effect.die(`spawn not supported in ${label}`)
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("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("harness");
148
147
  const stateRef = yield* SubscriptionRef.make(machine.initial);
149
148
  const hasPostponeRules = machine.postponeRules.length > 0;
150
149
  const postponed = [];