effect-machine 0.18.0 → 0.20.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,9 +1,10 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { getTag, makeDeferReply, makeReply } from "./internal/utils.js";
2
+ import { INTERNAL_INIT_EVENT, getTag, makeDeferReply, makeReply } from "./internal/utils.js";
3
3
  import { getReplySchemas } from "./schema.js";
4
4
  import { makeEventAdvancement } from "./internal/event-advancement.js";
5
5
  import { executeTransition, shouldPostpone } from "./internal/transition.js";
6
6
  import { emitWithTimestamp } from "./internal/inspection.js";
7
+ import { MachineInitialization } from "./internal/machine-initialization.js";
7
8
  import { Inspector } from "./inspection.js";
8
9
  import { ActorScope, createActor } from "./actor.js";
9
10
  import { Cause, Effect, Exit, Option, Random, Scope } from "effect";
@@ -14,6 +15,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
14
15
  make: () => make,
15
16
  replay: () => replay,
16
17
  reply: () => reply,
18
+ run: () => run,
17
19
  scoped: () => scoped,
18
20
  spawn: () => spawn
19
21
  });
@@ -34,6 +36,7 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
34
36
  return emitWithTimestamp(inspector.value, (timestamp) => ({
35
37
  type: "@machine.task",
36
38
  actorId: input.actorId,
39
+ generation: input.generation,
37
40
  state: input.state,
38
41
  taskName: input.taskName,
39
42
  phase: input.phase,
@@ -53,21 +56,27 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
53
56
  */
54
57
  var Machine = class Machine {
55
58
  initial;
59
+ #initialization;
56
60
  #backgroundEffects;
57
61
  #finalStates;
62
+ #finalOutputs;
58
63
  #postponeRules;
59
64
  stateSchema;
60
65
  eventSchema;
61
66
  #replySchemas;
62
67
  #transitionIndex;
68
+ #immediateIndex;
63
69
  #spawnIndex;
64
70
  /** @internal */
65
71
  constructor(initial, stateSchema, eventSchema) {
66
- this.initial = initial;
72
+ this.#initialization = MachineInitialization.make(initial);
73
+ this.initial = MachineInitialization.staticValue(this.#initialization);
67
74
  this.#backgroundEffects = [];
68
75
  this.#finalStates = /* @__PURE__ */ new Set();
76
+ this.#finalOutputs = /* @__PURE__ */ new Map();
69
77
  this.#postponeRules = [];
70
78
  this.#transitionIndex = /* @__PURE__ */ new Map();
79
+ this.#immediateIndex = /* @__PURE__ */ new Map();
71
80
  this.#spawnIndex = /* @__PURE__ */ new Map();
72
81
  this.#replySchemas = getReplySchemas(eventSchema) ?? /* @__PURE__ */ new Map();
73
82
  this.stateSchema = stateSchema;
@@ -79,8 +88,8 @@ var Machine = class Machine {
79
88
  return this;
80
89
  }
81
90
  /** @internal */
82
- scopeTransition(states, event, handler, reenter) {
83
- for (const state of states) this.addTransition(state, event, handler, reenter);
91
+ scopeTransition(states, event, handler, reenter, guard) {
92
+ for (const state of states) this.addTransition(state, event, handler, reenter, guard);
84
93
  return this;
85
94
  }
86
95
  on(stateOrStates, event, handler) {
@@ -88,6 +97,11 @@ var Machine = class Machine {
88
97
  for (const s of states) this.addTransition(s, event, handler, false);
89
98
  return this;
90
99
  }
100
+ when(stateOrStates, event, predicate, handler) {
101
+ const states = toReadonlyArray(stateOrStates);
102
+ for (const state of states) this.addTransition(state, event, handler, false, predicate);
103
+ return this;
104
+ }
91
105
  reenter(stateOrStates, event, handler) {
92
106
  let states;
93
107
  if (Array.isArray(stateOrStates)) states = stateOrStates;
@@ -95,6 +109,11 @@ var Machine = class Machine {
95
109
  for (const s of states) this.addTransition(s, event, handler, true);
96
110
  return this;
97
111
  }
112
+ reenterWhen(stateOrStates, event, predicate, handler) {
113
+ const states = toReadonlyArray(stateOrStates);
114
+ for (const state of states) this.addTransition(state, event, handler, true, predicate);
115
+ return this;
116
+ }
98
117
  /**
99
118
  * Register a wildcard transition that fires from any state when no specific transition matches.
100
119
  * Specific `.on()` transitions always take priority over `.onAny()`.
@@ -109,17 +128,59 @@ var Machine = class Machine {
109
128
  this.registerTransition(transition);
110
129
  return this;
111
130
  }
131
+ /** Register a conditional wildcard transition. */
132
+ whenAny(event, predicate, handler) {
133
+ const transition = {
134
+ stateTag: "*",
135
+ eventTag: getTag(event),
136
+ handler,
137
+ reenter: false,
138
+ guard: predicate
139
+ };
140
+ this.registerTransition(transition);
141
+ return this;
142
+ }
112
143
  /** @internal */
113
- addTransition(state, event, handler, reenter) {
144
+ addTransition(state, event, handler, reenter, guard) {
114
145
  const transition = {
115
146
  stateTag: getTag(state),
116
147
  eventTag: getTag(event),
117
148
  handler,
118
- reenter
149
+ reenter,
150
+ guard
119
151
  };
120
152
  this.registerTransition(transition);
121
153
  return this;
122
154
  }
155
+ immediate(stateOrStates, handler) {
156
+ const states = toReadonlyArray(stateOrStates);
157
+ for (const state of states) {
158
+ const stateTag = getTag(state);
159
+ const transitions = this.#immediateIndex.get(stateTag) ?? [];
160
+ transitions.push({
161
+ stateTag,
162
+ eventTag: "",
163
+ handler
164
+ });
165
+ this.#immediateIndex.set(stateTag, transitions);
166
+ }
167
+ return this;
168
+ }
169
+ immediateWhen(stateOrStates, predicate, handler) {
170
+ const states = toReadonlyArray(stateOrStates);
171
+ for (const state of states) {
172
+ const stateTag = getTag(state);
173
+ const transitions = this.#immediateIndex.get(stateTag) ?? [];
174
+ transitions.push({
175
+ stateTag,
176
+ eventTag: "",
177
+ handler,
178
+ guard: predicate
179
+ });
180
+ this.#immediateIndex.set(stateTag, transitions);
181
+ }
182
+ return this;
183
+ }
123
184
  spawn(stateOrStates, handler) {
124
185
  const states = toReadonlyArray(stateOrStates);
125
186
  for (const s of states) {
@@ -137,14 +198,20 @@ var Machine = class Machine {
137
198
  /** @internal */
138
199
  _findTransitions(stateTag, eventTag) {
139
200
  const specific = this.#transitionIndex.get(stateTag)?.get(eventTag) ?? [];
140
- if (specific.length > 0) return specific;
141
- return this.#transitionIndex.get("*")?.get(eventTag) ?? [];
201
+ const wildcard = this.#transitionIndex.get("*")?.get(eventTag) ?? [];
202
+ if (specific.length === 0) return wildcard;
203
+ if (wildcard.length === 0) return specific;
204
+ return [...specific, ...wildcard];
142
205
  }
143
206
  /** @internal */
144
207
  _findSpawnEffects(stateTag) {
145
208
  return this.#spawnIndex.get(stateTag) ?? [];
146
209
  }
147
210
  /** @internal */
211
+ _findImmediateTransitions(stateTag) {
212
+ return this.#immediateIndex.get(stateTag) ?? [];
213
+ }
214
+ /** @internal */
148
215
  _backgroundEffectEntries() {
149
216
  return this.#backgroundEffects.values();
150
217
  }
@@ -153,6 +220,16 @@ var Machine = class Machine {
153
220
  return this.#finalStates.has(stateTag);
154
221
  }
155
222
  /** @internal */
223
+ _initial(input) {
224
+ return MachineInitialization.resolve(this.#initialization, input);
225
+ }
226
+ /** @internal */
227
+ _output(state) {
228
+ const resolve = this.#finalOutputs.get(state._tag);
229
+ if (resolve === void 0) return state;
230
+ return resolve(state);
231
+ }
232
+ /** @internal */
156
233
  _shouldPostpone(stateTag, eventTag) {
157
234
  return this.#postponeRules.some((rule) => rule.stateTag === stateTag && rule.eventTag === eventTag);
158
235
  }
@@ -164,20 +241,6 @@ var Machine = class Machine {
164
241
  _replySchema(eventTag) {
165
242
  return this.#replySchemas.get(eventTag);
166
243
  }
167
- /** @internal */
168
- _withInitial(initial) {
169
- const copy = new Machine(initial, this.stateSchema, this.eventSchema);
170
- copy.#backgroundEffects.push(...this.#backgroundEffects);
171
- for (const stateTag of this.#finalStates) copy.#finalStates.add(stateTag);
172
- copy.#postponeRules.push(...this.#postponeRules);
173
- for (const [stateTag, events] of this.#transitionIndex) {
174
- const eventCopy = /* @__PURE__ */ new Map();
175
- for (const [eventTag, transitions] of events) eventCopy.set(eventTag, transitions.slice());
176
- copy.#transitionIndex.set(stateTag, eventCopy);
177
- }
178
- for (const [stateTag, effects] of this.#spawnIndex) copy.#spawnIndex.set(stateTag, effects.slice());
179
- return copy;
180
- }
181
244
  registerTransition(transition) {
182
245
  const events = this.#transitionIndex.get(transition.stateTag) ?? /* @__PURE__ */ new Map();
183
246
  const transitions = events.get(transition.eventTag) ?? [];
@@ -189,6 +252,7 @@ var Machine = class Machine {
189
252
  const handler = Effect.fn("effect-machine.task")(function* (ctx) {
190
253
  yield* emitTaskInspection({
191
254
  actorId: ctx.actorId,
255
+ generation: ctx.generation,
192
256
  state: ctx.state,
193
257
  taskName: options.name,
194
258
  phase: "start"
@@ -197,6 +261,7 @@ var Machine = class Machine {
197
261
  if (Exit.isSuccess(exit)) {
198
262
  yield* emitTaskInspection({
199
263
  actorId: ctx.actorId,
264
+ generation: ctx.generation,
200
265
  state: ctx.state,
201
266
  taskName: options.name,
202
267
  phase: "success"
@@ -211,21 +276,35 @@ var Machine = class Machine {
211
276
  if (Cause.hasInterruptsOnly(cause)) {
212
277
  yield* emitTaskInspection({
213
278
  actorId: ctx.actorId,
279
+ generation: ctx.generation,
214
280
  state: ctx.state,
215
281
  taskName: options.name,
216
282
  phase: "interrupt"
217
283
  });
218
284
  return;
219
285
  }
286
+ if (Cause.hasDies(cause) || Cause.hasInterrupts(cause)) {
287
+ yield* emitTaskInspection({
288
+ actorId: ctx.actorId,
289
+ generation: ctx.generation,
290
+ state: ctx.state,
291
+ taskName: options.name,
292
+ phase: "defect",
293
+ error: Cause.pretty(cause)
294
+ });
295
+ return yield* Effect.failCause(cause).pipe(Effect.orDie);
296
+ }
297
+ const error = Option.getOrThrow(Cause.findErrorOption(cause));
220
298
  yield* emitTaskInspection({
221
299
  actorId: ctx.actorId,
300
+ generation: ctx.generation,
222
301
  state: ctx.state,
223
302
  taskName: options.name,
224
303
  phase: "failure",
225
304
  error: Cause.pretty(cause)
226
305
  });
227
306
  if (options.onFailure !== void 0) {
228
- yield* ctx.self.send(options.onFailure(cause, ctx));
307
+ yield* ctx.self.send(options.onFailure(error, ctx));
229
308
  yield* Effect.yieldNow;
230
309
  return;
231
310
  }
@@ -316,9 +395,11 @@ var Machine = class Machine {
316
395
  }
317
396
  return this;
318
397
  }
319
- final(state) {
398
+ final(state, output) {
320
399
  const stateTag = getTag(state);
321
400
  this.#finalStates.add(stateTag);
401
+ if (output === void 0) this.#finalOutputs.set(stateTag, (finalState) => finalState);
402
+ else this.#finalOutputs.set(stateTag, (finalState) => output({ state: finalState }));
322
403
  return this;
323
404
  }
324
405
  static make(config) {
@@ -340,8 +421,34 @@ var TransitionScope = class {
340
421
  this.machine.scopeTransition(this.states, event, handler, true);
341
422
  return this;
342
423
  }
424
+ when(event, predicate, handler) {
425
+ this.machine.scopeTransition(this.states, event, handler, false, predicate);
426
+ return this;
427
+ }
428
+ reenterWhen(event, predicate, handler) {
429
+ this.machine.scopeTransition(this.states, event, handler, true, predicate);
430
+ return this;
431
+ }
343
432
  };
344
433
  const make = Machine.make;
434
+ const spawnImpl = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
435
+ let opts;
436
+ if (typeof idOrOptions === "string") opts = { id: idOrOptions };
437
+ else opts = idOrOptions;
438
+ const actorId = opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`;
439
+ const machineInitial = machine._initial(opts?.input);
440
+ const initialState = opts?.hydrate ?? machineInitial;
441
+ const actor = yield* createActor(actorId, machine, {
442
+ initialState,
443
+ machineInitial,
444
+ hydrated: opts?.hydrate !== void 0,
445
+ supervision: opts?.supervision,
446
+ lifecycle: opts?.lifecycle
447
+ });
448
+ const maybeScope = yield* Effect.serviceOption(ActorScope);
449
+ if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
450
+ return actor;
451
+ });
345
452
  /**
346
453
  * Spawn an actor from a machine.
347
454
  *
@@ -358,20 +465,10 @@ const make = Machine.make;
358
465
  * });
359
466
  * ```
360
467
  */
361
- const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
362
- let opts;
363
- if (typeof idOrOptions === "string") opts = { id: idOrOptions };
364
- else opts = idOrOptions;
365
- const actorId = opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`;
366
- const actor = yield* createActor(actorId, machine, {
367
- initialState: opts?.hydrate,
368
- supervision: opts?.supervision,
369
- lifecycle: opts?.lifecycle
370
- });
371
- const maybeScope = yield* Effect.serviceOption(ActorScope);
372
- if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
373
- return actor;
374
- });
468
+ const spawn = spawnImpl;
469
+ const runImpl = (machine, options) => Effect.acquireUseRelease(spawnImpl(machine, options), (actor) => actor.start.pipe(Effect.andThen(actor.awaitOutput)), (actor) => actor.stop);
470
+ /** Run one actor to final output and always release its resources. */
471
+ const run = runImpl;
375
472
  /**
376
473
  * Wrap an effect to provide an `ActorScope` from the current `Scope`.
377
474
  *
@@ -394,8 +491,10 @@ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions)
394
491
  const scoped = (effect) => Effect.flatMap(Effect.service(Scope.Scope), (scope) => Effect.provideService(effect, ActorScope, scope));
395
492
  const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
396
493
  const machine = input;
494
+ const from = options?.from ?? machine._initial(options?.input);
495
+ const initial = yield* executeTransition(machine, from, { _tag: INTERNAL_INIT_EVENT });
397
496
  const advancement = makeEventAdvancement({
398
- initial: options?.from ?? machine.initial,
497
+ initial: initial.newState,
399
498
  isFinal: (state) => machine._isFinal(state._tag),
400
499
  shouldPostpone: (state, event) => shouldPostpone(machine, state._tag, event._tag),
401
500
  postpone: (_state, event) => Effect.succeed({
@@ -416,4 +515,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (input, events, opti
416
515
  const reply = makeReply;
417
516
  const deferReply = makeDeferReply;
418
517
  //#endregion
419
- export { Machine, deferReply, machine_exports, make, replay, reply, scoped, spawn };
518
+ export { Machine, deferReply, machine_exports, make, replay, reply, run, scoped, spawn };
@@ -16,9 +16,10 @@ type DefectPhase = "transition" | "spawn" | "background" | "initial-spawn";
16
16
  * - `Stopped` — explicit `actor.stop` or `actor.drain`
17
17
  * - `Defect` — unhandled error in the runtime
18
18
  */
19
- type ActorExit<S> = {
19
+ type ActorExit<S, O = S> = {
20
20
  readonly _tag: "Final";
21
21
  readonly state: S;
22
+ readonly output: O;
22
23
  } | {
23
24
  readonly _tag: "Stopped";
24
25
  } | {
@@ -28,7 +29,7 @@ type ActorExit<S> = {
28
29
  };
29
30
  /** Constructors for ActorExit */
30
31
  declare const ActorExit: {
31
- readonly Final: <S>(state: S) => ActorExit<S>;
32
+ readonly Final: <S, O>(state: S, output: O) => ActorExit<S, O>;
32
33
  readonly Stopped: ActorExit<never>;
33
34
  readonly Defect: <S = never>(cause: Cause.Cause<unknown>, phase: DefectPhase) => ActorExit<S>;
34
35
  };
@@ -12,9 +12,10 @@ import { Schedule } from "effect";
12
12
  */
13
13
  /** Constructors for ActorExit */
14
14
  const ActorExit = {
15
- Final: (state) => ({
15
+ Final: (state, output) => ({
16
16
  _tag: "Final",
17
- state
17
+ state,
18
+ output
18
19
  }),
19
20
  Stopped: { _tag: "Stopped" },
20
21
  Defect: (cause, phase) => ({
package/dist/testing.d.ts CHANGED
@@ -2,7 +2,12 @@ import { AssertionError } from "./errors.js";
2
2
  import { Machine } from "./machine.js";
3
3
  import { Effect, SubscriptionRef } from "effect";
4
4
  //#region src/testing.d.ts
5
- type MachineInput<S, E, R> = Machine<S, E, R, any, any>;
5
+ type MachineInput<S, E, R, Input = void> = Machine<S, E, R, any, any, Input, any>;
6
+ type SimulationOptions<Input> = [Input] extends [void] ? {
7
+ readonly input?: never;
8
+ } : {
9
+ readonly input: Input;
10
+ };
6
11
  /**
7
12
  * Result of simulating events through a machine
8
13
  */
@@ -10,33 +15,18 @@ interface SimulationResult<S> {
10
15
  readonly states: ReadonlyArray<S>;
11
16
  readonly finalState: S;
12
17
  }
13
- /**
14
- * Simulate a sequence of events through a machine without running an actor.
15
- * Useful for testing state transitions in isolation.
16
- * Does not run task, spawn, or background effects.
17
- *
18
- * @example
19
- * ```ts
20
- * const result = yield* simulate(
21
- * fetcherMachine,
22
- * [
23
- * Event.Fetch({ url: "https://example.com" }),
24
- * Event._Done({ data: { foo: "bar" } })
25
- * ]
26
- * )
27
- *
28
- * expect(result.finalState._tag).toBe("Success")
29
- * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
30
- * ```
31
- */
32
- declare const simulate: <S extends {
33
- readonly _tag: string;
34
- }, E extends {
35
- readonly _tag: string;
36
- }, R>(input: MachineInput<S, E, R>, events: readonly E[]) => Effect.Effect<{
37
- states: S[];
38
- finalState: S;
39
- }, never, never>;
18
+ declare const simulate: {
19
+ <S extends {
20
+ readonly _tag: string;
21
+ }, E extends {
22
+ readonly _tag: string;
23
+ }, R>(input: MachineInput<S, E, R, void>, events: ReadonlyArray<E>, options?: SimulationOptions<void>): Effect.Effect<SimulationResult<S>, never, R>;
24
+ <S extends {
25
+ readonly _tag: string;
26
+ }, E extends {
27
+ readonly _tag: string;
28
+ }, R, Input>(input: MachineInput<S, E, R, Input>, events: ReadonlyArray<E>, options: SimulationOptions<Input>): Effect.Effect<SimulationResult<S>, never, R>;
29
+ };
40
30
  /**
41
31
  * Assert that a machine can reach a specific state given a sequence of events
42
32
  */
@@ -44,7 +34,7 @@ declare const assertReaches: <S extends {
44
34
  readonly _tag: string;
45
35
  }, E extends {
46
36
  readonly _tag: string;
47
- }, R>(input: MachineInput<S, E, R>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, never>;
37
+ }, R>(input: MachineInput<S, E, R, void>, events: readonly E[], expectedTag: string) => Effect.Effect<S, AssertionError, R>;
48
38
  /**
49
39
  * Assert that a machine follows a specific path of state tags
50
40
  *
@@ -61,10 +51,7 @@ declare const assertPath: <S extends {
61
51
  readonly _tag: string;
62
52
  }, E extends {
63
53
  readonly _tag: string;
64
- }, R>(input: MachineInput<S, E, R>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<{
65
- states: S[];
66
- finalState: S;
67
- }, AssertionError, never>;
54
+ }, R>(input: MachineInput<S, E, R, void>, events: readonly E[], expectedPath: readonly string[]) => Effect.Effect<SimulationResult<S>, AssertionError, R>;
68
55
  /**
69
56
  * Assert that a machine never reaches a specific state given a sequence of events
70
57
  *
@@ -82,10 +69,7 @@ declare const assertNeverReaches: <S extends {
82
69
  readonly _tag: string;
83
70
  }, E extends {
84
71
  readonly _tag: string;
85
- }, R>(input: MachineInput<S, E, R>, events: readonly E[], forbiddenTag: string) => Effect.Effect<{
86
- states: S[];
87
- finalState: S;
88
- }, AssertionError, never>;
72
+ }, R>(input: MachineInput<S, E, R, void>, events: readonly E[], forbiddenTag: string) => Effect.Effect<SimulationResult<S>, AssertionError, R>;
89
73
  /**
90
74
  * Create a controllable test harness for a machine
91
75
  */
@@ -104,34 +88,22 @@ interface TestHarnessOptions<S, E> {
104
88
  */
105
89
  readonly onTransition?: (from: S, event: E, to: S) => void;
106
90
  }
107
- /**
108
- * Create a test harness for step-by-step testing.
109
- * Does not run task, spawn, or background effects.
110
- *
111
- * @example Basic usage
112
- * ```ts
113
- * const harness = yield* createTestHarness(machine)
114
- * yield* harness.send(Event.Start())
115
- * const state = yield* harness.getState
116
- * ```
117
- *
118
- * @example With transition observer
119
- * ```ts
120
- * const transitions: Array<{ from: string; event: string; to: string }> = []
121
- * const harness = yield* createTestHarness(machine, {
122
- * onTransition: (from, event, to) =>
123
- * transitions.push({ from: from._tag, event: event._tag, to: to._tag })
124
- * })
125
- * ```
126
- */
127
- declare const createTestHarness: <S extends {
128
- readonly _tag: string;
129
- }, E extends {
130
- readonly _tag: string;
131
- }, R>(input: MachineInput<S, E, R>, options?: TestHarnessOptions<S, E> | undefined) => Effect.Effect<{
132
- state: SubscriptionRef.SubscriptionRef<S>;
133
- send: (event: E) => Effect.Effect<S, never, never>;
134
- getState: Effect.Effect<S, never, never>;
135
- }, never, never>;
91
+ type InputTestHarnessOptions<S, E, Input> = TestHarnessOptions<S, E> & ([Input] extends [void] ? {
92
+ readonly input?: never;
93
+ } : {
94
+ readonly input: Input;
95
+ });
96
+ declare const createTestHarness: {
97
+ <S extends {
98
+ readonly _tag: string;
99
+ }, E extends {
100
+ readonly _tag: string;
101
+ }, R>(input: MachineInput<S, E, R, void>, options?: InputTestHarnessOptions<S, E, void>): Effect.Effect<TestHarness<S, E>, never, R>;
102
+ <S extends {
103
+ readonly _tag: string;
104
+ }, E extends {
105
+ readonly _tag: string;
106
+ }, R, Input>(input: MachineInput<S, E, R, Input>, options: InputTestHarnessOptions<S, E, Input>): Effect.Effect<TestHarness<S, E>, never, R>;
107
+ };
136
108
  //#endregion
137
- export { AssertionError, SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
109
+ export { AssertionError, InputTestHarnessOptions, SimulationOptions, SimulationResult, TestHarness, TestHarnessOptions, assertNeverReaches, assertPath, assertReaches, createTestHarness, simulate };
package/dist/testing.js CHANGED
@@ -1,39 +1,18 @@
1
- import { isEffect } from "./internal/utils.js";
1
+ import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
2
2
  import { AssertionError } from "./errors.js";
3
3
  import { makeEventAdvancement } from "./internal/event-advancement.js";
4
- import { executeTransition, executeTransitionImmediate, shouldPostpone } from "./internal/transition.js";
4
+ import { executeTransition, shouldPostpone } from "./internal/transition.js";
5
5
  import { Effect, SubscriptionRef } from "effect";
6
- //#region src/testing.ts
7
- /**
8
- * Simulate a sequence of events through a machine without running an actor.
9
- * Useful for testing state transitions in isolation.
10
- * Does not run task, spawn, or background effects.
11
- *
12
- * @example
13
- * ```ts
14
- * const result = yield* simulate(
15
- * fetcherMachine,
16
- * [
17
- * Event.Fetch({ url: "https://example.com" }),
18
- * Event._Done({ data: { foo: "bar" } })
19
- * ]
20
- * )
21
- *
22
- * expect(result.finalState._tag).toBe("Success")
23
- * expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
24
- * ```
25
- */
26
- const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
6
+ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events, options) {
27
7
  const machine = input;
28
- const states = [machine.initial];
8
+ const machineInitial = machine._initial(options?.input);
9
+ const initial = yield* executeTransition(machine, machineInitial, { _tag: INTERNAL_INIT_EVENT });
10
+ const states = [initial.newState];
29
11
  if (!machine._hasPostponeRules()) {
30
- let state = machine.initial;
12
+ let state = initial.newState;
31
13
  for (const event of events) {
32
14
  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;
15
+ const result = yield* executeTransition(machine, state, event);
37
16
  if (result.transitioned) {
38
17
  state = result.newState;
39
18
  states.push(state);
@@ -45,7 +24,7 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
45
24
  };
46
25
  }
47
26
  const advancement = makeEventAdvancement({
48
- initial: machine.initial,
27
+ initial: initial.newState,
49
28
  isFinal: (state) => machine._isFinal(state._tag),
50
29
  shouldPostpone: (state, event) => shouldPostpone(machine, state._tag, event._tag),
51
30
  postpone: (_state, event) => Effect.succeed({
@@ -115,31 +94,13 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
115
94
  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(" -> ")}` });
116
95
  return result;
117
96
  });
118
- /**
119
- * Create a test harness for step-by-step testing.
120
- * Does not run task, spawn, or background effects.
121
- *
122
- * @example Basic usage
123
- * ```ts
124
- * const harness = yield* createTestHarness(machine)
125
- * yield* harness.send(Event.Start())
126
- * const state = yield* harness.getState
127
- * ```
128
- *
129
- * @example With transition observer
130
- * ```ts
131
- * const transitions: Array<{ from: string; event: string; to: string }> = []
132
- * const harness = yield* createTestHarness(machine, {
133
- * onTransition: (from, event, to) =>
134
- * transitions.push({ from: from._tag, event: event._tag, to: to._tag })
135
- * })
136
- * ```
137
- */
138
97
  const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
139
98
  const machine = input;
140
- const stateRef = yield* SubscriptionRef.make(machine.initial);
99
+ const machineInitial = machine._initial(options?.input);
100
+ const initial = yield* executeTransition(machine, machineInitial, { _tag: INTERNAL_INIT_EVENT });
101
+ const stateRef = yield* SubscriptionRef.make(initial.newState);
141
102
  const advancement = makeEventAdvancement({
142
- initial: machine.initial,
103
+ initial: initial.newState,
143
104
  isFinal: (state) => machine._isFinal(state._tag),
144
105
  shouldPostpone: (state, event) => shouldPostpone(machine, state._tag, event._tag),
145
106
  postpone: (state, event) => Effect.succeed({