effect-machine 0.17.0 → 0.18.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -54
  3. package/dist/actor.d.ts +8 -45
  4. package/dist/actor.js +157 -231
  5. package/dist/cluster/adapters/in-memory.d.ts +0 -1
  6. package/dist/cluster/adapters/in-memory.js +10 -5
  7. package/dist/cluster/entity-actor-ref.d.ts +2 -3
  8. package/dist/cluster/entity-actor-ref.js +14 -17
  9. package/dist/cluster/entity-machine.d.ts +2 -8
  10. package/dist/cluster/entity-machine.js +57 -34
  11. package/dist/cluster/index.js +2 -2
  12. package/dist/cluster/persistence.d.ts +0 -1
  13. package/dist/cluster/to-entity.d.ts +19 -21
  14. package/dist/cluster/to-entity.js +18 -19
  15. package/dist/errors.d.ts +11 -37
  16. package/dist/errors.js +12 -30
  17. package/dist/index.d.ts +4 -5
  18. package/dist/index.js +4 -5
  19. package/dist/inspection.d.ts +0 -1
  20. package/dist/inspection.js +24 -13
  21. package/dist/internal/brands.d.ts +0 -1
  22. package/dist/internal/event-advancement.d.ts +50 -0
  23. package/dist/internal/event-advancement.js +79 -0
  24. package/dist/internal/inspection.d.ts +5 -9
  25. package/dist/internal/inspection.js +31 -10
  26. package/dist/internal/machine-definition.d.ts +16 -0
  27. package/dist/internal/runtime.d.ts +1 -161
  28. package/dist/internal/runtime.js +191 -150
  29. package/dist/internal/transition.d.ts +4 -135
  30. package/dist/internal/transition.js +82 -157
  31. package/dist/internal/utils.d.ts +3 -40
  32. package/dist/internal/utils.js +2 -2
  33. package/dist/machine.d.ts +47 -137
  34. package/dist/machine.js +147 -215
  35. package/dist/schema.d.ts +42 -21
  36. package/dist/schema.js +28 -18
  37. package/dist/supervision.d.ts +1 -24
  38. package/dist/supervision.js +2 -3
  39. package/dist/testing.d.ts +15 -29
  40. package/dist/testing.js +76 -95
  41. package/package.json +19 -38
  42. package/dist/slot.d.ts +0 -159
  43. package/dist/slot.js +0 -165
  44. package/v3/dist/_virtual/_rolldown/runtime.js +0 -13
  45. package/v3/dist/actor.d.ts +0 -250
  46. package/v3/dist/actor.js +0 -577
  47. package/v3/dist/cluster/adapters/in-memory.d.ts +0 -15
  48. package/v3/dist/cluster/adapters/in-memory.js +0 -62
  49. package/v3/dist/cluster/entity-actor-ref.d.ts +0 -49
  50. package/v3/dist/cluster/entity-actor-ref.js +0 -19
  51. package/v3/dist/cluster/entity-machine.d.ts +0 -74
  52. package/v3/dist/cluster/entity-machine.js +0 -166
  53. package/v3/dist/cluster/index.d.ts +0 -6
  54. package/v3/dist/cluster/index.js +0 -6
  55. package/v3/dist/cluster/persistence.d.ts +0 -48
  56. package/v3/dist/cluster/persistence.js +0 -14
  57. package/v3/dist/cluster/to-entity.d.ts +0 -69
  58. package/v3/dist/cluster/to-entity.js +0 -59
  59. package/v3/dist/errors.d.ts +0 -95
  60. package/v3/dist/errors.js +0 -54
  61. package/v3/dist/index.d.ts +0 -11
  62. package/v3/dist/index.js +0 -9
  63. package/v3/dist/inspection.d.ts +0 -151
  64. package/v3/dist/inspection.js +0 -128
  65. package/v3/dist/internal/brands.d.ts +0 -50
  66. package/v3/dist/internal/inspection.d.ts +0 -11
  67. package/v3/dist/internal/inspection.js +0 -20
  68. package/v3/dist/internal/runtime.d.ts +0 -161
  69. package/v3/dist/internal/runtime.js +0 -360
  70. package/v3/dist/internal/transition.d.ts +0 -190
  71. package/v3/dist/internal/transition.js +0 -278
  72. package/v3/dist/internal/utils.d.ts +0 -101
  73. package/v3/dist/internal/utils.js +0 -75
  74. package/v3/dist/machine.d.ts +0 -398
  75. package/v3/dist/machine.js +0 -487
  76. package/v3/dist/schema.d.ts +0 -174
  77. package/v3/dist/schema.js +0 -206
  78. package/v3/dist/slot.d.ts +0 -158
  79. package/v3/dist/slot.js +0 -165
  80. package/v3/dist/supervision.d.ts +0 -97
  81. package/v3/dist/supervision.js +0 -42
  82. package/v3/dist/testing.d.ts +0 -151
  83. package/v3/dist/testing.js +0 -189
  84. /package/{v3/dist/internal/brands.js → dist/internal/machine-definition.js} +0 -0
package/dist/schema.js CHANGED
@@ -4,7 +4,7 @@ import { Schema } from "effect";
4
4
  /**
5
5
  * Schema-first State/Event definitions for effect-machine.
6
6
  *
7
- * MachineSchema provides a single source of truth that combines:
7
+ * `State` and `Event` provide one source of truth that combines:
8
8
  * - Schema for validation/serialization
9
9
  * - Variant constructors (like Data.taggedEnum)
10
10
  * - $is and $match helpers for pattern matching
@@ -19,6 +19,9 @@ import { Schema } from "effect";
19
19
  * Pending: { orderId: Schema.String },
20
20
  * Shipped: { trackingId: Schema.String },
21
21
  * })
22
+ * const OrderEvent = Event({
23
+ * Ship: { trackingId: Schema.String },
24
+ * })
22
25
  *
23
26
  * // Infer type from schema
24
27
  * type OrderState = typeof OrderState.Type
@@ -32,17 +35,24 @@ import { Schema } from "effect";
32
35
  * Shipped: (s) => `Shipped: ${s.trackingId}`,
33
36
  * })
34
37
  *
35
- * // Use as Schema for persistence/cluster
36
- * machine.pipe(Machine.persist({ stateSchema: OrderState, ... }))
38
+ * // Use the schemas to define a machine
39
+ * const machine = Machine.make({
40
+ * state: OrderState,
41
+ * event: OrderEvent,
42
+ * initial: pending,
43
+ * })
37
44
  * ```
38
45
  *
39
46
  * @module
40
47
  */
41
48
  const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
49
+ const replySchemaRegistry = /* @__PURE__ */ new WeakMap();
50
+ /** @internal */
51
+ const getReplySchemas = (schema) => replySchemaRegistry.get(schema);
42
52
  /**
43
53
  * Build a schema-first definition from a record of tag -> fields
44
54
  */
45
- const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
55
+ const RESERVED_DERIVE_KEYS = /* @__PURE__ */ new Set(["_tag"]);
46
56
  const buildMachineSchema = (definition) => {
47
57
  const variants = {};
48
58
  const constructors = {};
@@ -79,20 +89,22 @@ const buildMachineSchema = (definition) => {
79
89
  };
80
90
  }
81
91
  const variantArray = Object.values(variants);
82
- if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
83
- const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
92
+ if (variantArray.length === 0) throw InvalidSchemaError.make({ message: "Schema must have at least one variant" });
93
+ let unionSchema;
94
+ if (variantArray.length === 1) unionSchema = variantArray[0];
95
+ else unionSchema = Schema.Union(variantArray);
84
96
  const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
85
97
  const $match = (valueOrCases, maybeCases) => {
86
98
  if (maybeCases !== void 0) {
87
99
  const value = valueOrCases;
88
100
  const handler = maybeCases[value._tag];
89
- if (handler === void 0) throw new MissingMatchHandlerError({ tag: value._tag });
101
+ if (handler === void 0) throw MissingMatchHandlerError.make({ tag: value._tag });
90
102
  return handler(value);
91
103
  }
92
104
  const cases = valueOrCases;
93
105
  return (value) => {
94
106
  const handler = cases[value._tag];
95
- if (handler === void 0) throw new MissingMatchHandlerError({ tag: value._tag });
107
+ if (handler === void 0) throw MissingMatchHandlerError.make({ tag: value._tag });
96
108
  return handler(value);
97
109
  };
98
110
  };
@@ -100,7 +112,6 @@ const buildMachineSchema = (definition) => {
100
112
  schema: unionSchema,
101
113
  variants,
102
114
  constructors,
103
- _definition: definition,
104
115
  replySchemas,
105
116
  $is,
106
117
  $match
@@ -111,24 +122,23 @@ const buildMachineSchema = (definition) => {
111
122
  * Builds the schema object with variants, constructors, $is, and $match.
112
123
  */
113
124
  const createMachineSchema = (definition) => {
114
- const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
125
+ const { schema, variants, constructors, replySchemas, $is, $match } = buildMachineSchema(definition);
115
126
  const withFn = (source, partial) => {
116
127
  const ctor = constructors[source._tag];
117
- if (ctor === void 0) throw new MissingMatchHandlerError({ tag: source._tag });
128
+ if (ctor === void 0) throw MissingMatchHandlerError.make({ tag: source._tag });
118
129
  const fn = ctor.with;
119
- if (fn === void 0) throw new MissingMatchHandlerError({ tag: source._tag });
130
+ if (fn === void 0) throw MissingMatchHandlerError.make({ tag: source._tag });
120
131
  return fn(source, partial);
121
132
  };
122
- return Object.assign(Object.create(schema), {
133
+ const machineSchema = Object.assign(Object.create(schema), {
123
134
  variants,
124
- _definition,
125
- _replySchemas: replySchemas,
126
- schema,
127
135
  $is,
128
136
  $match,
129
137
  with: withFn,
130
138
  ...constructors
131
139
  });
140
+ replySchemaRegistry.set(machineSchema, replySchemas);
141
+ return machineSchema;
132
142
  };
133
143
  /**
134
144
  * Create a schema-first State definition.
@@ -139,7 +149,7 @@ const createMachineSchema = (definition) => {
139
149
  *
140
150
  * @example
141
151
  * ```ts
142
- * const OrderState = MachineSchema.State({
152
+ * const OrderState = State({
143
153
  * Pending: { orderId: Schema.String },
144
154
  * Shipped: { trackingId: Schema.String },
145
155
  * })
@@ -203,4 +213,4 @@ const replyFieldsFn = (fields, replySchema) => {
203
213
  };
204
214
  const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
205
215
  //#endregion
206
- export { Event, State };
216
+ export { Event, State, getReplySchemas };
@@ -1,5 +1,4 @@
1
1
  import { Cause, Duration, Schedule } from "effect";
2
-
3
2
  //#region src/supervision.d.ts
4
3
  /**
5
4
  * Where in the actor lifecycle a defect occurred.
@@ -33,28 +32,6 @@ declare const ActorExit: {
33
32
  readonly Stopped: ActorExit<never>;
34
33
  readonly Defect: <S = never>(cause: Cause.Cause<unknown>, phase: DefectPhase) => ActorExit<S>;
35
34
  };
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
35
  declare namespace Supervision {
59
36
  /**
60
37
  * Supervision policy for actor restart behavior.
@@ -94,4 +71,4 @@ declare namespace Supervision {
94
71
  }) => Policy;
95
72
  }
96
73
  //#endregion
97
- export { ActorExit, CellPhase, DefectPhase, Supervision };
74
+ export { ActorExit, DefectPhase, Supervision };
@@ -7,7 +7,6 @@ import { Schedule } from "effect";
7
7
  * - `ActorExit<S>` — why an actor stopped (final, explicit stop, or defect)
8
8
  * - `DefectPhase` — where in the lifecycle a defect occurred
9
9
  * - `Supervision.Policy` — Schedule-based restart policy
10
- * - `CellPhase<S>` — internal phase machine for serializing stop/restart/drain
11
10
  *
12
11
  * @module
13
12
  */
@@ -31,10 +30,10 @@ let Supervision;
31
30
  let schedule = Schedule.forever;
32
31
  if (options?.maxRestarts !== void 0) {
33
32
  const recurs = Schedule.recurs(options.maxRestarts);
34
- if (options.within !== void 0) schedule = Schedule.both(recurs, Schedule.windowed(options.within));
33
+ if (options.within !== void 0) schedule = Schedule.min([recurs, Schedule.windowed(options.within)]);
35
34
  else schedule = recurs;
36
35
  }
37
- if (options?.backoff !== void 0) schedule = Schedule.both(schedule, options.backoff);
36
+ if (options?.backoff !== void 0) schedule = Schedule.min([schedule, options.backoff]);
38
37
  return { schedule };
39
38
  };
40
39
  })(Supervision || (Supervision = {}));
package/dist/testing.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { AssertionError } from "./errors.js";
2
- import { MachineContextTag, ProvideSlots, SlotsDef } from "./slot.js";
3
2
  import { Machine } from "./machine.js";
4
3
  import { Effect, SubscriptionRef } from "effect";
5
-
6
4
  //#region src/testing.d.ts
7
- type MachineInput<S, E, R, SD extends SlotsDef = Record<string, never>> = Machine<S, E, R, any, any, SD>;
5
+ type MachineInput<S, E, R> = Machine<S, E, R, any, any>;
8
6
  /**
9
7
  * Result of simulating events through a machine
10
8
  */
@@ -15,8 +13,7 @@ interface SimulationResult<S> {
15
13
  /**
16
14
  * Simulate a sequence of events through a machine without running an actor.
17
15
  * Useful for testing state transitions in isolation.
18
- * Does not run onEnter/spawn/background effects, but does run slots
19
- * within transition handlers.
16
+ * Does not run task, spawn, or background effects.
20
17
  *
21
18
  * @example
22
19
  * ```ts
@@ -36,12 +33,10 @@ declare const simulate: <S extends {
36
33
  readonly _tag: string;
37
34
  }, E extends {
38
35
  readonly _tag: string;
39
- }, R, SD extends SlotsDef = Record<string, never>>(input: MachineInput<S, E, R, SD>, events: readonly E[], options?: {
40
- slots?: ProvideSlots<SD, any>;
41
- } | undefined) => Effect.Effect<{
36
+ }, R>(input: MachineInput<S, E, R>, events: readonly E[]) => Effect.Effect<{
42
37
  states: S[];
43
38
  finalState: S;
44
- }, never, Exclude<R, MachineContextTag>>;
39
+ }, never, never>;
45
40
  /**
46
41
  * Assert that a machine can reach a specific state given a sequence of events
47
42
  */
@@ -49,9 +44,7 @@ declare const assertReaches: <S extends {
49
44
  readonly _tag: string;
50
45
  }, E extends {
51
46
  readonly _tag: string;
52
- }, R, SD extends SlotsDef = Record<string, never>>(input: MachineInput<S, E, R, SD>, events: readonly E[], expectedTag: string, options?: {
53
- slots?: ProvideSlots<SD, any>;
54
- } | undefined) => Effect.Effect<S, AssertionError, Exclude<R, MachineContextTag>>;
47
+ }, R>(input: MachineInput<S, E, R>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, never>;
55
48
  /**
56
49
  * Assert that a machine follows a specific path of state tags
57
50
  *
@@ -68,12 +61,10 @@ declare const assertPath: <S extends {
68
61
  readonly _tag: string;
69
62
  }, E extends {
70
63
  readonly _tag: string;
71
- }, R, SD extends SlotsDef = Record<string, never>>(input: MachineInput<S, E, R, SD>, events: readonly E[], expectedPath: readonly string[], options?: {
72
- slots?: ProvideSlots<SD, any>;
73
- } | undefined) => Effect.Effect<{
64
+ }, R>(input: MachineInput<S, E, R>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
74
65
  states: S[];
75
66
  finalState: S;
76
- }, AssertionError, Exclude<R, MachineContextTag>>;
67
+ }, AssertionError, never>;
77
68
  /**
78
69
  * Assert that a machine never reaches a specific state given a sequence of events
79
70
  *
@@ -91,36 +82,31 @@ declare const assertNeverReaches: <S extends {
91
82
  readonly _tag: string;
92
83
  }, E extends {
93
84
  readonly _tag: string;
94
- }, R, SD extends SlotsDef = Record<string, never>>(input: MachineInput<S, E, R, SD>, events: readonly E[], forbiddenTag: string, options?: {
95
- slots?: ProvideSlots<SD, any>;
96
- } | undefined) => Effect.Effect<{
85
+ }, R>(input: MachineInput<S, E, R>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
97
86
  states: S[];
98
87
  finalState: S;
99
- }, AssertionError, Exclude<R, MachineContextTag>>;
88
+ }, AssertionError, never>;
100
89
  /**
101
90
  * Create a controllable test harness for a machine
102
91
  */
103
- interface TestHarness<S, E, R> {
92
+ interface TestHarness<S, E> {
104
93
  readonly state: SubscriptionRef.SubscriptionRef<S>;
105
- readonly send: (event: E) => Effect.Effect<S, never, R>;
94
+ readonly send: (event: E) => Effect.Effect<S>;
106
95
  readonly getState: Effect.Effect<S>;
107
96
  }
108
97
  /**
109
98
  * Options for creating a test harness
110
99
  */
111
- interface TestHarnessOptions<S, E, SD extends SlotsDef = Record<string, never>> {
100
+ interface TestHarnessOptions<S, E> {
112
101
  /**
113
102
  * Called after each transition with the previous state, event, and new state.
114
103
  * Useful for logging or spying on transitions.
115
104
  */
116
105
  readonly onTransition?: (from: S, event: E, to: S) => void;
117
- /** Slot handler implementations. */
118
- readonly slots?: ProvideSlots<SD, any>;
119
106
  }
120
107
  /**
121
108
  * Create a test harness for step-by-step testing.
122
- * Does not run onEnter/spawn/background effects, but does run slots
123
- * within transition handlers.
109
+ * Does not run task, spawn, or background effects.
124
110
  *
125
111
  * @example Basic usage
126
112
  * ```ts
@@ -142,9 +128,9 @@ declare const createTestHarness: <S extends {
142
128
  readonly _tag: string;
143
129
  }, E extends {
144
130
  readonly _tag: string;
145
- }, R, SD extends SlotsDef = Record<string, never>>(input: MachineInput<S, E, R, SD>, options?: TestHarnessOptions<S, E, SD> | undefined) => Effect.Effect<{
131
+ }, R>(input: MachineInput<S, E, R>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
146
132
  state: SubscriptionRef.SubscriptionRef<S>;
147
- send: (event: E) => Effect.Effect<S, never, Exclude<R, MachineContextTag>>;
133
+ send: (event: E) => Effect.Effect<S, never, never>;
148
134
  getState: Effect.Effect<S, never, never>;
149
135
  }, never, never>;
150
136
  //#endregion
package/dist/testing.js CHANGED
@@ -1,23 +1,13 @@
1
- import { stubSystem } from "./internal/utils.js";
1
+ import { isEffect } from "./internal/utils.js";
2
2
  import { AssertionError } from "./errors.js";
3
- import { executeTransition, shouldPostpone } from "./internal/transition.js";
4
- import { materializeMachine } from "./machine.js";
3
+ import { makeEventAdvancement } from "./internal/event-advancement.js";
4
+ import { executeTransition, executeTransitionImmediate, shouldPostpone } from "./internal/transition.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
- };
16
7
  /**
17
8
  * Simulate a sequence of events through a machine without running an actor.
18
9
  * Useful for testing state transitions in isolation.
19
- * Does not run onEnter/spawn/background effects, but does run slots
20
- * within transition handlers.
10
+ * Does not run task, spawn, or background effects.
21
11
  *
22
12
  * @example
23
13
  * ```ts
@@ -33,53 +23,58 @@ const makeDummySelf = (label) => {
33
23
  * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
34
24
  * ```
35
25
  */
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");
39
- let currentState = machine.initial;
40
- const states = [currentState];
41
- const hasPostponeRules = machine.postponeRules.length > 0;
42
- const postponed = [];
43
- for (const event of events) {
44
- if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
45
- postponed.push(event);
46
- continue;
47
- }
48
- const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "simulation");
49
- if (!result.transitioned) continue;
50
- const prevTag = currentState._tag;
51
- currentState = result.newState;
52
- states.push(currentState);
53
- if (machine.finalStates.has(currentState._tag)) break;
54
- let drainTag = prevTag;
55
- while (currentState._tag !== drainTag && postponed.length > 0) {
56
- drainTag = currentState._tag;
57
- const drained = postponed.splice(0);
58
- for (const postponedEvent of drained) {
59
- if (shouldPostpone(machine, currentState._tag, postponedEvent._tag)) {
60
- postponed.push(postponedEvent);
61
- continue;
62
- }
63
- const drainResult = yield* executeTransition(machine, currentState, postponedEvent, dummySelf, stubSystem, "simulation");
64
- if (drainResult.transitioned) {
65
- currentState = drainResult.newState;
66
- states.push(currentState);
67
- if (machine.finalStates.has(currentState._tag)) break;
68
- }
26
+ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
27
+ const machine = input;
28
+ const states = [machine.initial];
29
+ if (!machine._hasPostponeRules()) {
30
+ let state = machine.initial;
31
+ for (const event of events) {
32
+ if (machine._isFinal(state._tag)) break;
33
+ const execution = executeTransitionImmediate(machine, state, event);
34
+ let result;
35
+ if (isEffect(execution)) result = yield* execution;
36
+ else result = execution;
37
+ if (result.transitioned) {
38
+ state = result.newState;
39
+ states.push(state);
69
40
  }
70
41
  }
42
+ return {
43
+ states,
44
+ finalState: state
45
+ };
71
46
  }
47
+ const advancement = makeEventAdvancement({
48
+ initial: machine.initial,
49
+ isFinal: (state) => machine._isFinal(state._tag),
50
+ shouldPostpone: (state, event) => shouldPostpone(machine, state._tag, event._tag),
51
+ postpone: (_state, event) => Effect.succeed({
52
+ input: event,
53
+ value: void 0
54
+ }),
55
+ process: (state, event) => executeTransition(machine, state, event).pipe(Effect.map((result) => {
56
+ if (result.transitioned) states.push(result.newState);
57
+ return {
58
+ state: result.newState,
59
+ transitioned: result.transitioned,
60
+ stateChanged: result.transitioned && (result.newState._tag !== state._tag || result.reenter),
61
+ shouldStop: result.transitioned && machine._isFinal(result.newState._tag),
62
+ value: void 0
63
+ };
64
+ }))
65
+ });
66
+ for (const event of events) yield* advancement.advance(event);
72
67
  return {
73
68
  states,
74
- finalState: currentState
69
+ finalState: advancement.state
75
70
  };
76
71
  });
77
72
  /**
78
73
  * Assert that a machine can reach a specific state given a sequence of events
79
74
  */
80
- const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag, options) {
81
- const result = yield* simulate(input, events, options);
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(" -> ")}` });
75
+ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
76
+ const result = yield* simulate(input, events);
77
+ if (result.finalState._tag !== expectedTag) return yield* AssertionError.make({ message: `Expected final state "${expectedTag}" but got "${result.finalState._tag}". States visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
83
78
  return result.finalState;
84
79
  });
85
80
  /**
@@ -94,11 +89,11 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
94
89
  * )
95
90
  * ```
96
91
  */
97
- const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath, options) {
98
- const result = yield* simulate(input, events, options);
92
+ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
93
+ const result = yield* simulate(input, events);
99
94
  const actualPath = result.states.map((s) => s._tag);
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(" -> ")}` });
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(" -> ")}` });
95
+ if (actualPath.length !== expectedPath.length) return yield* AssertionError.make({ message: `Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
96
+ for (let i = 0; i < expectedPath.length; i++) if (actualPath[i] !== expectedPath[i]) return yield* AssertionError.make({ message: `Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
102
97
  return result;
103
98
  });
104
99
  /**
@@ -114,16 +109,15 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
114
109
  * )
115
110
  * ```
116
111
  */
117
- const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag, options) {
118
- const result = yield* simulate(input, events, options);
112
+ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
113
+ const result = yield* simulate(input, events);
119
114
  const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
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(" -> ")}` });
115
+ if (visitedIndex !== -1) return yield* AssertionError.make({ message: `Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\nStates visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
121
116
  return result;
122
117
  });
123
118
  /**
124
119
  * Create a test harness for step-by-step testing.
125
- * Does not run onEnter/spawn/background effects, but does run slots
126
- * within transition handlers.
120
+ * Does not run task, spawn, or background effects.
127
121
  *
128
122
  * @example Basic usage
129
123
  * ```ts
@@ -142,45 +136,32 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
142
136
  * ```
143
137
  */
144
138
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
145
- const machine = materializeMachine(input, options?.slots);
146
- const dummySelf = makeDummySelf("effect-machine.testing.harness");
139
+ const machine = input;
147
140
  const stateRef = yield* SubscriptionRef.make(machine.initial);
148
- const hasPostponeRules = machine.postponeRules.length > 0;
149
- const postponed = [];
141
+ const advancement = makeEventAdvancement({
142
+ initial: machine.initial,
143
+ isFinal: (state) => machine._isFinal(state._tag),
144
+ shouldPostpone: (state, event) => shouldPostpone(machine, state._tag, event._tag),
145
+ postpone: (state, event) => Effect.succeed({
146
+ input: event,
147
+ value: state
148
+ }),
149
+ process: (state, event) => executeTransition(machine, state, event).pipe(Effect.map((result) => ({
150
+ state: result.newState,
151
+ transitioned: result.transitioned,
152
+ stateChanged: result.transitioned && (result.newState._tag !== state._tag || result.reenter),
153
+ shouldStop: result.transitioned && machine._isFinal(result.newState._tag),
154
+ value: result.newState
155
+ }))),
156
+ commit: (state, event, step) => {
157
+ if (!step.transitioned) return Effect.void;
158
+ return SubscriptionRef.set(stateRef, step.state).pipe(Effect.tap(() => Effect.sync(() => options?.onTransition?.(state, event, step.state))));
159
+ }
160
+ });
150
161
  return {
151
162
  state: stateRef,
152
163
  send: Effect.fn("effect-machine.testHarness.send")(function* (event) {
153
- const currentState = yield* SubscriptionRef.get(stateRef);
154
- if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
155
- postponed.push(event);
156
- return currentState;
157
- }
158
- const result = yield* executeTransition(machine, currentState, event, dummySelf, stubSystem, "test-harness");
159
- if (!result.transitioned) return currentState;
160
- const prevTag = currentState._tag;
161
- const newState = result.newState;
162
- yield* SubscriptionRef.set(stateRef, newState);
163
- if (options?.onTransition !== void 0) options.onTransition(currentState, event, newState);
164
- let drainTag = prevTag;
165
- let currentTag = newState._tag;
166
- while (currentTag !== drainTag && postponed.length > 0) {
167
- drainTag = currentTag;
168
- const drained = postponed.splice(0);
169
- for (const postponedEvent of drained) {
170
- const state = yield* SubscriptionRef.get(stateRef);
171
- if (shouldPostpone(machine, state._tag, postponedEvent._tag)) {
172
- postponed.push(postponedEvent);
173
- continue;
174
- }
175
- const drainResult = yield* executeTransition(machine, state, postponedEvent, dummySelf, stubSystem, "test-harness");
176
- if (drainResult.transitioned) {
177
- yield* SubscriptionRef.set(stateRef, drainResult.newState);
178
- currentTag = drainResult.newState._tag;
179
- if (options?.onTransition !== void 0) options.onTransition(state, postponedEvent, drainResult.newState);
180
- }
181
- }
182
- }
183
- return newState;
164
+ return (yield* advancement.advance(event)).state;
184
165
  }),
185
166
  getState: SubscriptionRef.get(stateRef)
186
167
  };
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "effect-machine",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cevr/effect-machine.git"
7
7
  },
8
8
  "files": [
9
- "dist",
10
- "v3/dist"
9
+ "dist"
11
10
  ],
12
11
  "type": "module",
13
12
  "exports": {
@@ -22,61 +21,43 @@
22
21
  "types": "./dist/cluster/index.d.ts",
23
22
  "default": "./dist/cluster/index.js"
24
23
  }
25
- },
26
- "./v3": {
27
- "import": {
28
- "types": "./v3/dist/index.d.ts",
29
- "default": "./v3/dist/index.js"
30
- }
31
- },
32
- "./v3/cluster": {
33
- "import": {
34
- "types": "./v3/dist/cluster/index.d.ts",
35
- "default": "./v3/dist/cluster/index.js"
36
- }
37
24
  }
38
25
  },
39
26
  "publishConfig": {
40
27
  "access": "public"
41
28
  },
42
29
  "scripts": {
43
- "typecheck": "concurrently -n v4,v3 -c blue,magenta \"tsgo --noEmit\" \"bun run typecheck:v3\"",
44
- "typecheck:v3": "tsgo --noEmit -p v3/tsconfig.json",
30
+ "typecheck": "tsc --noEmit",
45
31
  "lint": "oxlint",
46
32
  "lint:fix": "oxlint --fix",
47
33
  "fmt": "oxfmt",
48
34
  "fmt:check": "oxfmt --check",
49
35
  "test": "bun test --tsconfig-override=tsconfig.json",
50
- "test:v3": "cd v3 && bun test --tsconfig-override=tsconfig.json ./test/",
51
36
  "test:watch": "bun test --watch",
52
- "test:all": "bun run test && bun run test:v3",
53
- "gate": "concurrently -n type,lint,fmt,test,build -c blue,yellow,magenta,green,cyan \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test:all\" \"bun run build\"",
37
+ "bench": "bun benchmark/core.ts",
38
+ "gate": "concurrently -n type,lint,fmt,test,build -c blue,yellow,magenta,green,cyan \"bun run typecheck\" \"bun run lint:fix\" \"bun run fmt\" \"bun run test\" \"bun run build\"",
54
39
  "prepare": "lefthook install && effect-tsgo patch",
55
40
  "version": "changeset version",
56
- "build": "concurrently -n v4,v3 -c cyan,magenta \"tsdown\" \"tsdown --config v3/tsdown.config.ts\"",
41
+ "build": "bun --bun tsdown",
57
42
  "release": "bun run build && changeset publish"
58
43
  },
59
44
  "devDependencies": {
60
- "@changesets/changelog-github": "^0.6.0",
61
- "@changesets/cli": "^2.30.0",
62
- "@effect/cluster": "0.58.2",
63
- "@effect/rpc": "0.75.1",
64
- "@effect/tsgo": "0.5.2",
65
- "@types/bun": "1.3.12",
66
- "@typescript/native-preview": "7.0.0-dev.20260507.1",
67
- "concurrently": "^9.2.1",
68
- "effect": "4.0.0-beta.63",
45
+ "@changesets/changelog-github": "1.0.0",
46
+ "@changesets/cli": "3.0.1",
47
+ "@effect/tsgo": "0.38.0",
48
+ "@types/bun": "1.4.0",
49
+ "concurrently": "10.0.5",
50
+ "effect": "4.0.0-rc.112",
69
51
  "effect-bun-test": "0.3.0",
70
- "effect-v3": "npm:effect@3.21.2",
71
- "lefthook": "^2.1.5",
72
- "oxfmt": "^0.44.0",
73
- "oxlint": "^1.63.0",
74
- "oxlint-tsgolint": "^0.22.1",
75
- "tsdown": "^0.21.7",
76
- "typescript": "^6.0.2"
52
+ "lefthook": "2.1.12",
53
+ "oxfmt": "0.65.0",
54
+ "oxlint": "1.80.0",
55
+ "oxlint-plugin-effect": "0.11.0",
56
+ "tsdown": "0.22.14",
57
+ "typescript": "7.0.2"
77
58
  },
78
59
  "peerDependencies": {
79
- "effect": ">=4.0.0-beta.63"
60
+ "effect": ">=4.0.0-rc.112 <5"
80
61
  },
81
62
  "peerDependenciesMeta": {
82
63
  "effect": {