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.
@@ -1,15 +1,16 @@
1
- import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
1
+ import { Supervision } from "./supervision.js";
2
2
  import { ReplyResult, TransitionResult } from "./internal/utils.js";
3
3
  import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
4
4
  import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
5
5
  import { DuplicateActorError } from "./errors.js";
6
+ import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
6
7
  import { findTransitions } from "./internal/transition.js";
7
8
  import { ActorRef, ActorSystem } from "./actor.js";
8
9
  import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
9
10
 
10
11
  //#region src/machine.d.ts
11
12
  declare namespace machine_d_exports {
12
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, reply, spawn };
13
+ export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, materializeMachine, replay, reply, spawn };
13
14
  }
14
15
  /**
15
16
  * Self reference for sending events back to the machine
@@ -22,7 +23,7 @@ interface MachineRef<Event> {
22
23
  readonly _tag: string;
23
24
  }, E2 extends {
24
25
  readonly _tag: string;
25
- }, R2>(id: string, machine: BuiltMachine<S2, E2, R2>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
26
+ }, R2>(id: string, machine: Machine<S2, E2, R2, any, any, any, any>) => Effect.Effect<ActorRef<S2, E2>, DuplicateActorError, R2>;
26
27
  }
27
28
  /**
28
29
  * Handler context passed to transition handlers
@@ -93,9 +94,6 @@ interface TimeoutConfig<State, Event> {
93
94
  /** Event to send when the timer fires. Static or derived from current state. */
94
95
  readonly event: Event | ((state: State) => Event);
95
96
  }
96
- type IsAny<T> = 0 extends 1 & T ? true : false;
97
- type IsUnknown<T> = unknown extends T ? ([T] extends [unknown] ? true : false) : false;
98
- type NormalizeR<T> = IsAny<T> extends true ? T : IsUnknown<T> extends true ? never : T;
99
97
  interface MakeConfig<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef, EFD extends EffectsDef> {
100
98
  readonly state: MachineStateSchema<SD> & {
101
99
  Type: S;
@@ -115,22 +113,14 @@ type HasEffectKeys<EFD extends EffectsDef> = [keyof EFD] extends [never] ? false
115
113
  type SlotContext<State, Event> = MachineContext<State, Event, MachineRef<Event>>;
116
114
  /** Combined handlers for build() - guards and effects only */
117
115
  type ProvideHandlers<State, Event, GD extends GuardsDef, EFD extends EffectsDef, R> = (HasGuardKeys<GD> extends true ? GuardHandlers<GD, SlotContext<State, Event>, R> : object) & (HasEffectKeys<EFD> extends true ? EffectHandlers<EFD, SlotContext<State, Event>, R> : object);
118
- /** Whether the machine has any guard or effect slots */
119
- type HasSlots<GD extends GuardsDef, EFD extends EffectsDef> = HasGuardKeys<GD> extends true ? true : HasEffectKeys<EFD>;
120
116
  /**
121
- * A finalized machine ready for spawning.
117
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
118
+ * If no handlers provided and machine has no slots, returns the machine as-is.
119
+ * Validates that all required slots are provided and no extra slots are given.
122
120
  *
123
- * Created by calling `.build()` on a `Machine`. This is the only type
124
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
125
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
121
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
126
122
  */
127
- declare class BuiltMachine<State, Event, R = never> {
128
- /** @internal */
129
- readonly _inner: Machine<State, Event, R, any, any, any, any>;
130
- /** @internal */
131
- constructor(machine: Machine<State, Event, R, any, any, any, any>);
132
- get initial(): State;
133
- }
123
+ declare const materializeMachine: <S, E, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, any, any, GD, EFD>, handlers?: Record<string, any>) => Machine<S, E, never, any, any, GD, EFD>;
134
124
  /**
135
125
  * Machine definition with fluent builder API.
136
126
  *
@@ -306,13 +296,6 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
306
296
  */
307
297
  postpone<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, events: TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent> | ReadonlyArray<TaggedOrConstructor<VariantsUnion<_ED> & BrandedEvent>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
308
298
  final<NS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
309
- /**
310
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
311
- *
312
- * - Machines with slots: pass implementations as the first argument.
313
- * - Machines without slots: call with no arguments.
314
- */
315
- build<R2 = never>(...args: HasSlots<GD, EFD> extends true ? [handlers: ProvideHandlers<State, Event, GD, EFD, R2>] : [handlers?: ProvideHandlers<State, Event, GD, EFD, R2>]): BuiltMachine<State, Event, R | NormalizeR<R2>>;
316
299
  static make<SD extends Record<string, Schema.Struct.Fields>, ED extends Record<string, Schema.Struct.Fields>, S extends BrandedState, E extends BrandedEvent, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(config: MakeConfig<SD, ED, S, E, GD, EFD>): Machine<S, E, never, SD, ED, GD, EFD>;
317
300
  }
318
301
  declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema.Struct.Fields>, _ED extends Record<string, Schema.Struct.Fields>, GD extends GuardsDef, EFD extends EffectsDef, SelectedState extends VariantsUnion<_SD> & BrandedState> {
@@ -323,34 +306,33 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
323
306
  reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
324
307
  }
325
308
  declare const make: typeof Machine.make;
309
+ type AnyMachine<S, E, R> = Machine<S, E, R, any, any, any, any>;
326
310
  /**
327
- * Spawn an actor from a built machine.
311
+ * Spawn an actor from a machine.
328
312
  *
329
313
  * Options:
330
314
  * - `id` — custom actor ID (default: random)
331
315
  * - `hydrate` — restore from a previously-saved state snapshot.
332
- * The actor starts in the hydrated state and re-runs spawn effects
333
- * for that state (timers, scoped resources, etc.). Transition history
334
- * is not replayed — only the current state's entry effects run.
335
- *
336
- * Persistence is composed in userland by observing `actor.changes`
337
- * and saving snapshots to your own storage.
316
+ * - `slots` slot handler implementations for slotful machines.
338
317
  */
339
318
  declare const spawn: <S extends {
340
319
  readonly _tag: string;
341
320
  }, E extends {
342
321
  readonly _tag: string;
343
- }, R>(machine: BuiltMachine<S, E, R>, idOrOptions?: string | {
322
+ }, R>(machine: AnyMachine<S, E, R>, options?: string | {
344
323
  id?: string;
345
324
  hydrate?: S;
325
+ slots?: Record<string, any>;
326
+ supervision?: Supervision.Policy;
346
327
  }) => Effect.Effect<ActorRef<S, E>, never, R>;
347
328
  declare const replay: <S extends {
348
329
  readonly _tag: string;
349
330
  }, E extends {
350
331
  readonly _tag: string;
351
- }, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
332
+ }, R>(machine: AnyMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
352
333
  from?: S;
334
+ slots?: Record<string, any>;
353
335
  }) => Effect.Effect<S, never, R>;
354
336
  declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
355
337
  //#endregion
356
- export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, reply, spawn };
338
+ export { BackgroundEffect, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, materializeMachine, replay, reply, spawn };
@@ -1,18 +1,18 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
- import { Inspector } from "./inspection.js";
3
2
  import { getTag, makeReply, stubSystem } from "./internal/utils.js";
4
3
  import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
4
+ import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
5
5
  import { emitWithTimestamp } from "./internal/inspection.js";
6
+ import { Inspector } from "./inspection.js";
6
7
  import { MachineContextTag } from "./slot.js";
7
- import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
8
8
  import { createActor } from "./actor.js";
9
9
  import { Cause, Effect, Exit, Option, 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
  findTransitions: () => findTransitions,
15
14
  make: () => make,
15
+ materializeMachine: () => materializeMachine,
16
16
  replay: () => replay,
17
17
  reply: () => reply,
18
18
  spawn: () => spawn
@@ -27,22 +27,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Insp
27
27
  timestamp
28
28
  })));
29
29
  /**
30
- * A finalized machine ready for spawning.
30
+ * Bind slot handlers to a machine, returning a fresh copy with handlers installed.
31
+ * If no handlers provided and machine has no slots, returns the machine as-is.
32
+ * Validates that all required slots are provided and no extra slots are given.
31
33
  *
32
- * Created by calling `.build()` on a `Machine`. This is the only type
33
- * accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
34
- * Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
34
+ * @internal used by spawn, replay, simulate, test harness, entity-machine
35
35
  */
36
- var BuiltMachine = class {
37
- /** @internal */
38
- _inner;
39
- /** @internal */
40
- constructor(machine) {
41
- this._inner = machine;
42
- }
43
- get initial() {
44
- return this._inner.initial;
36
+ const materializeMachine = (machine, handlers) => {
37
+ if (handlers === void 0) {
38
+ const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
39
+ const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
40
+ if (hasGuards || hasEffects) {
41
+ const missing = [];
42
+ if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
43
+ if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
44
+ throw new ProvisionValidationError({
45
+ missing,
46
+ extra: []
47
+ });
48
+ }
49
+ return machine;
45
50
  }
51
+ const requiredSlots = /* @__PURE__ */ new Set();
52
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
53
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
54
+ const providedSlots = new Set(Object.keys(handlers));
55
+ const missing = [];
56
+ const extra = [];
57
+ for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
58
+ for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
59
+ if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
60
+ missing,
61
+ extra
62
+ });
63
+ const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
64
+ result._transitions = [...machine._transitions];
65
+ result._finalStates = new Set(machine._finalStates);
66
+ result._spawnEffects = [...machine._spawnEffects];
67
+ result._backgroundEffects = [...machine._backgroundEffects];
68
+ result._postponeRules = [...machine._postponeRules];
69
+ result._replySchemas = machine._replySchemas;
70
+ if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
71
+ if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
72
+ return result;
46
73
  };
47
74
  /**
48
75
  * Machine definition with fluent builder API.
@@ -350,41 +377,6 @@ var Machine = class Machine {
350
377
  this._finalStates.add(stateTag);
351
378
  return this;
352
379
  }
353
- /**
354
- * Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
355
- *
356
- * - Machines with slots: pass implementations as the first argument.
357
- * - Machines without slots: call with no arguments.
358
- */
359
- build(...args) {
360
- const handlers = args[0];
361
- if (handlers !== void 0) {
362
- const requiredSlots = /* @__PURE__ */ new Set();
363
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
364
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
365
- const providedSlots = new Set(Object.keys(handlers));
366
- const missing = [];
367
- const extra = [];
368
- for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
369
- for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
370
- if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
371
- missing,
372
- extra
373
- });
374
- const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
375
- result._transitions = [...this._transitions];
376
- result._finalStates = new Set(this._finalStates);
377
- result._spawnEffects = [...this._spawnEffects];
378
- result._backgroundEffects = [...this._backgroundEffects];
379
- result._postponeRules = [...this._postponeRules];
380
- result._replySchemas = this._replySchemas;
381
- const anyHandlers = handlers;
382
- if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
383
- if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
384
- return new BuiltMachine(result);
385
- }
386
- return new BuiltMachine(this);
387
- }
388
380
  static make(config) {
389
381
  return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
390
382
  }
@@ -405,27 +397,25 @@ var TransitionScope = class {
405
397
  };
406
398
  const make = Machine.make;
407
399
  /**
408
- * Spawn an actor from a built machine.
400
+ * Spawn an actor from a machine.
409
401
  *
410
402
  * Options:
411
403
  * - `id` — custom actor ID (default: random)
412
404
  * - `hydrate` — restore from a previously-saved state snapshot.
413
- * The actor starts in the hydrated state and re-runs spawn effects
414
- * for that state (timers, scoped resources, etc.). Transition history
415
- * is not replayed — only the current state's entry effects run.
416
- *
417
- * Persistence is composed in userland by observing `actor.changes`
418
- * and saving snapshots to your own storage.
405
+ * - `slots` slot handler implementations for slotful machines.
419
406
  */
420
- const spawn = Effect.fn("effect-machine.spawn")(function* (built, idOrOptions) {
421
- const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
422
- const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, built._inner, { initialState: opts?.hydrate });
407
+ const spawn = Effect.fn("effect-machine.spawn")(function* (machine, options) {
408
+ const opts = typeof options === "string" ? { id: options } : options;
409
+ const actor = yield* createActor(opts?.id ?? `actor-${Math.random().toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
410
+ initialState: opts?.hydrate,
411
+ supervision: opts?.supervision
412
+ });
423
413
  const maybeScope = yield* Effect.serviceOption(Scope.Scope);
424
414
  if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
425
415
  return actor;
426
416
  });
427
- const replay = Effect.fn("effect-machine.replay")(function* (built, events, options) {
428
- const machine = built._inner;
417
+ const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
418
+ const machine = materializeMachine(input, options?.slots);
429
419
  let state = options?.from ?? machine.initial;
430
420
  const hasPostponeRules = machine.postponeRules.length > 0;
431
421
  const postponed = [];
@@ -469,4 +459,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
469
459
  });
470
460
  const reply = makeReply;
471
461
  //#endregion
472
- export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, reply, spawn };
462
+ export { Machine, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
@@ -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 = [];