effect-machine 0.12.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.
package/dist/machine.js CHANGED
@@ -1,19 +1,19 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Inspector } from "./inspection.js";
3
3
  import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
4
- import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
5
- import { emitWithTimestamp } from "./internal/inspection.js";
6
- import { MachineContextTag } from "./slot.js";
7
4
  import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
+ import { emitWithTimestamp } from "./internal/inspection.js";
6
+ import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
8
7
  import { createActor } from "./actor.js";
9
- import { Cause, Effect, Exit, Option, Scope } from "effect";
8
+ import { MachineContextTag } from "./slot.js";
9
+ import { Cause, Effect, Exit, Option, Random, Scope } from "effect";
10
10
  //#region src/machine.ts
11
11
  var machine_exports = /* @__PURE__ */ __exportAll({
12
- BuiltMachine: () => BuiltMachine,
13
12
  Machine: () => Machine,
14
13
  deferReply: () => deferReply,
15
14
  findTransitions: () => findTransitions,
16
15
  make: () => make,
16
+ materializeMachine: () => materializeMachine,
17
17
  replay: () => replay,
18
18
  reply: () => reply,
19
19
  spawn: () => spawn
@@ -28,22 +28,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
28
28
  timestamp
29
29
  })));
30
30
  /**
31
- * A finalized machine ready for spawning.
31
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
32
+ * If no handlers provided and machine has no slots, returns the machine as-is.
33
+ * Validates that all required slots are provided and no extra slots are given.
32
34
  *
33
- * Created by calling `.build()` on a `Machine`. This is the only type
34
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
35
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
35
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
36
36
  */
37
- var BuiltMachine = class {
38
- /** @internal */
39
- _inner;
40
- /** @internal */
41
- constructor(machine) {
42
- this._inner = machine;
43
- }
44
- get initial() {
45
- return this._inner.initial;
37
+ const materializeMachine = (machine, handlers) => {
38
+ if (handlers === void 0) {
39
+ const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
40
+ const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
41
+ if (hasGuards || hasEffects) {
42
+ const missing = [];
43
+ if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
44
+ if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
45
+ throw new ProvisionValidationError({
46
+ missing,
47
+ extra: []
48
+ });
49
+ }
50
+ return machine;
46
51
  }
52
+ const requiredSlots = /* @__PURE__ */ new Set();
53
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
54
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
55
+ const providedSlots = new Set(Object.keys(handlers));
56
+ const missing = [];
57
+ const extra = [];
58
+ for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
59
+ for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
60
+ if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
61
+ missing,
62
+ extra
63
+ });
64
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
65
+ result._transitions = [...machine._transitions];
66
+ result._finalStates = new Set(machine._finalStates);
67
+ result._spawnEffects = [...machine._spawnEffects];
68
+ result._backgroundEffects = [...machine._backgroundEffects];
69
+ result._postponeRules = [...machine._postponeRules];
70
+ result._replySchemas = machine._replySchemas;
71
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
72
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
73
+ return result;
47
74
  };
48
75
  /**
49
76
  * Machine definition with fluent builder API.
@@ -355,41 +382,6 @@ var Machine = class Machine {
355
382
  this._finalStates.add(stateTag);
356
383
  return this;
357
384
  }
358
- /**
359
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
360
- *
361
- * - Machines with slots: pass implementations as the first argument.
362
- * - Machines without slots: call with no arguments.
363
- */
364
- build(...args) {
365
- const handlers = args[0];
366
- if (handlers !== void 0) {
367
- const requiredSlots = /* @__PURE__ */ new Set();
368
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
369
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
370
- const providedSlots = new Set(Object.keys(handlers));
371
- const missing = [];
372
- const extra = [];
373
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
374
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
375
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
376
- missing,
377
- extra
378
- });
379
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
380
- result._transitions = [...this._transitions];
381
- result._finalStates = new Set(this._finalStates);
382
- result._spawnEffects = [...this._spawnEffects];
383
- result._backgroundEffects = [...this._backgroundEffects];
384
- result._postponeRules = [...this._postponeRules];
385
- result._replySchemas = this._replySchemas;
386
- const anyHandlers = handlers;
387
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
388
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
389
- return new BuiltMachine(result);
390
- }
391
- return new BuiltMachine(this);
392
- }
393
385
  static make(config) {
394
386
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
395
387
  }
@@ -410,27 +402,36 @@ var TransitionScope = class {
410
402
  };
411
403
  const make = Machine.make;
412
404
  /**
413
- * Spawn an actor from a built machine.
405
+ * Spawn an actor from a machine.
406
+ *
407
+ * For machines with slots, pass implementations via `{ slots: { ... } }`.
408
+ *
409
+ * @example
410
+ * ```ts
411
+ * // No slots
412
+ * const actor = yield* Machine.spawn(machine);
414
413
  *
415
- * Options:
416
- * - `id` custom actor ID (default: random)
417
- * - `hydrate` restore from a previously-saved state snapshot.
418
- * The actor starts in the hydrated state and re-runs spawn effects
419
- * for that state (timers, scoped resources, etc.). Transition history
420
- * is not replayed — only the current state's entry effects run.
414
+ * // With slots
415
+ * const actor = yield* Machine.spawn(machine, {
416
+ * slots: { canRetry: ({ max }, { state }) => state.attempts < max },
417
+ * });
421
418
  *
422
- * Persistence is composed in userland by observing `actor.changes`
423
- * and saving snapshots to your own storage.
419
+ * // With hydration
420
+ * const actor = yield* Machine.spawn(machine, { hydrate: savedState });
421
+ * ```
424
422
  */
425
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
423
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
426
424
  const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
427
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
425
+ const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
426
+ initialState: opts?.hydrate,
427
+ supervision: opts?.supervision
428
+ });
428
429
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
429
430
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
430
431
  return actor;
431
432
  });
432
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
433
- const machine = built._inner;
433
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
434
+ const machine = materializeMachine(input, options?.slots);
434
435
  let state = options?.from ?? machine.initial;
435
436
  const hasPostponeRules = machine.postponeRules.length > 0;
436
437
  const postponed = [];
@@ -476,4 +477,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
476
477
  const reply = makeReply;
477
478
  const deferReply = makeDeferReply;
478
479
  //#endregion
479
- export { BuiltMachine, Machine, deferReply, findTransitions, machine_exports, make, replay, reply, spawn };
480
+ export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
package/dist/schema.js CHANGED
@@ -78,7 +78,7 @@ const buildMachineSchema = (definition) => {
78
78
  };
79
79
  }
80
80
  const variantArray = Object.values(variants);
81
- if (variantArray.length === 0) throw new InvalidSchemaError({});
81
+ if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
82
82
  const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
83
83
  const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
84
84
  const $match = (valueOrCases, maybeCases) => {
@@ -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,15 +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
- reply: () => Effect.succeed(false)
35
- };
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");
36
39
  let currentState = machine.initial;
37
40
  const states = [currentState];
38
41
  const hasPostponeRules = machine.postponeRules.length > 0;
@@ -74,8 +77,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
74
77
  /**
75
78
  * Assert that a machine can reach a specific state given a sequence of events
76
79
  */
77
- const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
78
- 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);
79
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(" -> ")}` });
80
83
  return result.finalState;
81
84
  });
@@ -91,8 +94,8 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
91
94
  * )
92
95
  * ```
93
96
  */
94
- const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
95
- 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);
96
99
  const actualPath = result.states.map((s) => s._tag);
97
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(" -> ")}` });
98
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(" -> ")}` });
@@ -111,8 +114,8 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
111
114
  * )
112
115
  * ```
113
116
  */
114
- const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
115
- 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);
116
119
  const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
117
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(" -> ")}` });
118
121
  return result;
@@ -139,14 +142,8 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
139
142
  * ```
140
143
  */
141
144
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
142
- const machine = input instanceof BuiltMachine ? input._inner : input;
143
- const dummySend = Effect.fn("effect-machine.testing.harness.send")((_event) => Effect.void);
144
- const dummySelf = {
145
- send: dummySend,
146
- cast: dummySend,
147
- spawn: () => Effect.die("spawn not supported in test harness"),
148
- reply: () => Effect.succeed(false)
149
- };
145
+ const machine = materializeMachine(input, options?.slots);
146
+ const dummySelf = makeDummySelf("effect-machine.testing.harness");
150
147
  const stateRef = yield* SubscriptionRef.make(machine.initial);
151
148
  const hasPostponeRules = machine.postponeRules.length > 0;
152
149
  const postponed = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
@@ -61,15 +61,15 @@
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": {