effect-machine 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -324
- package/dist/actor.d.ts +42 -27
- package/dist/actor.js +212 -305
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +2 -2
- package/dist/errors.d.ts +16 -19
- package/dist/errors.js +3 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/internal/runtime.d.ts +81 -6
- package/dist/internal/runtime.js +166 -57
- package/dist/internal/transition.d.ts +5 -4
- package/dist/internal/transition.js +9 -9
- package/dist/machine.d.ts +65 -44
- package/dist/machine.js +68 -67
- package/dist/schema.js +1 -1
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +17 -8
- package/dist/testing.js +22 -25
- package/package.json +5 -5
- package/v3/dist/actor.d.ts +50 -34
- package/v3/dist/actor.js +209 -289
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/errors.d.ts +3 -8
- package/v3/dist/errors.js +2 -4
- package/v3/dist/index.d.ts +5 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/runtime.d.ts +82 -5
- package/v3/dist/internal/runtime.js +147 -48
- package/v3/dist/internal/transition.d.ts +5 -4
- package/v3/dist/internal/transition.js +8 -8
- package/v3/dist/machine.d.ts +18 -36
- package/v3/dist/machine.js +54 -64
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +18 -9
- package/v3/dist/testing.js +21 -22
package/dist/machine.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { Inspector } from "./inspection.js";
|
|
3
3
|
import { getTag, makeDeferReply, makeReply, stubSystem } from "./internal/utils.js";
|
|
4
|
-
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
5
|
-
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
|
-
import { MachineContextTag } from "./slot.js";
|
|
7
4
|
import { findTransitions, invalidateIndex, resolveTransition, runTransitionHandler, shouldPostpone } from "./internal/transition.js";
|
|
5
|
+
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
|
+
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
8
7
|
import { createActor } from "./actor.js";
|
|
9
|
-
import {
|
|
8
|
+
import { MachineContextTag } from "./slot.js";
|
|
9
|
+
import { Cause, Effect, Exit, Option, Random, Scope } from "effect";
|
|
10
10
|
//#region src/machine.ts
|
|
11
11
|
var machine_exports = /* @__PURE__ */ __exportAll({
|
|
12
|
-
BuiltMachine: () => BuiltMachine,
|
|
13
12
|
Machine: () => Machine,
|
|
14
13
|
deferReply: () => deferReply,
|
|
15
14
|
findTransitions: () => findTransitions,
|
|
16
15
|
make: () => make,
|
|
16
|
+
materializeMachine: () => materializeMachine,
|
|
17
17
|
replay: () => replay,
|
|
18
18
|
reply: () => reply,
|
|
19
19
|
spawn: () => spawn
|
|
@@ -28,22 +28,49 @@ const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOption(Inspec
|
|
|
28
28
|
timestamp
|
|
29
29
|
})));
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Bind slot handlers to a machine, returning a fresh copy with handlers installed.
|
|
32
|
+
* If no handlers provided and machine has no slots, returns the machine as-is.
|
|
33
|
+
* Validates that all required slots are provided and no extra slots are given.
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
* accepted by `Machine.spawn` and `ActorSystem.spawn` (regular overload).
|
|
35
|
-
* Testing utilities (`simulate`, `createTestHarness`, etc.) still accept `Machine`.
|
|
35
|
+
* @internal — used by spawn, replay, simulate, test harness, entity-machine
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
37
|
+
const materializeMachine = (machine, handlers) => {
|
|
38
|
+
if (handlers === void 0) {
|
|
39
|
+
const hasGuards = machine._guardsSchema !== void 0 && Object.keys(machine._guardsSchema.definitions).length > 0;
|
|
40
|
+
const hasEffects = machine._effectsSchema !== void 0 && Object.keys(machine._effectsSchema.definitions).length > 0;
|
|
41
|
+
if (hasGuards || hasEffects) {
|
|
42
|
+
const missing = [];
|
|
43
|
+
if (machine._guardsSchema !== void 0) missing.push(...Object.keys(machine._guardsSchema.definitions));
|
|
44
|
+
if (machine._effectsSchema !== void 0) missing.push(...Object.keys(machine._effectsSchema.definitions));
|
|
45
|
+
throw new ProvisionValidationError({
|
|
46
|
+
missing,
|
|
47
|
+
extra: []
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return machine;
|
|
46
51
|
}
|
|
52
|
+
const requiredSlots = /* @__PURE__ */ new Set();
|
|
53
|
+
if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) requiredSlots.add(name);
|
|
54
|
+
if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) requiredSlots.add(name);
|
|
55
|
+
const providedSlots = new Set(Object.keys(handlers));
|
|
56
|
+
const missing = [];
|
|
57
|
+
const extra = [];
|
|
58
|
+
for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
|
|
59
|
+
for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
|
|
60
|
+
if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
|
|
61
|
+
missing,
|
|
62
|
+
extra
|
|
63
|
+
});
|
|
64
|
+
const result = new Machine(machine.initial, machine.stateSchema, machine.eventSchema, machine._guardsSchema, machine._effectsSchema);
|
|
65
|
+
result._transitions = [...machine._transitions];
|
|
66
|
+
result._finalStates = new Set(machine._finalStates);
|
|
67
|
+
result._spawnEffects = [...machine._spawnEffects];
|
|
68
|
+
result._backgroundEffects = [...machine._backgroundEffects];
|
|
69
|
+
result._postponeRules = [...machine._postponeRules];
|
|
70
|
+
result._replySchemas = machine._replySchemas;
|
|
71
|
+
if (machine._guardsSchema !== void 0) for (const name of Object.keys(machine._guardsSchema.definitions)) result._guardHandlers.set(name, handlers[name]);
|
|
72
|
+
if (machine._effectsSchema !== void 0) for (const name of Object.keys(machine._effectsSchema.definitions)) result._effectHandlers.set(name, handlers[name]);
|
|
73
|
+
return result;
|
|
47
74
|
};
|
|
48
75
|
/**
|
|
49
76
|
* Machine definition with fluent builder API.
|
|
@@ -355,41 +382,6 @@ var Machine = class Machine {
|
|
|
355
382
|
this._finalStates.add(stateTag);
|
|
356
383
|
return this;
|
|
357
384
|
}
|
|
358
|
-
/**
|
|
359
|
-
* Finalize the machine. Returns a `BuiltMachine` — the only type accepted by `Machine.spawn`.
|
|
360
|
-
*
|
|
361
|
-
* - Machines with slots: pass implementations as the first argument.
|
|
362
|
-
* - Machines without slots: call with no arguments.
|
|
363
|
-
*/
|
|
364
|
-
build(...args) {
|
|
365
|
-
const handlers = args[0];
|
|
366
|
-
if (handlers !== void 0) {
|
|
367
|
-
const requiredSlots = /* @__PURE__ */ new Set();
|
|
368
|
-
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) requiredSlots.add(name);
|
|
369
|
-
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) requiredSlots.add(name);
|
|
370
|
-
const providedSlots = new Set(Object.keys(handlers));
|
|
371
|
-
const missing = [];
|
|
372
|
-
const extra = [];
|
|
373
|
-
for (const name of requiredSlots) if (!providedSlots.has(name)) missing.push(name);
|
|
374
|
-
for (const name of providedSlots) if (!requiredSlots.has(name)) extra.push(name);
|
|
375
|
-
if (missing.length > 0 || extra.length > 0) throw new ProvisionValidationError({
|
|
376
|
-
missing,
|
|
377
|
-
extra
|
|
378
|
-
});
|
|
379
|
-
const result = new Machine(this.initial, this.stateSchema, this.eventSchema, this._guardsSchema, this._effectsSchema);
|
|
380
|
-
result._transitions = [...this._transitions];
|
|
381
|
-
result._finalStates = new Set(this._finalStates);
|
|
382
|
-
result._spawnEffects = [...this._spawnEffects];
|
|
383
|
-
result._backgroundEffects = [...this._backgroundEffects];
|
|
384
|
-
result._postponeRules = [...this._postponeRules];
|
|
385
|
-
result._replySchemas = this._replySchemas;
|
|
386
|
-
const anyHandlers = handlers;
|
|
387
|
-
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
|
|
388
|
-
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
|
|
389
|
-
return new BuiltMachine(result);
|
|
390
|
-
}
|
|
391
|
-
return new BuiltMachine(this);
|
|
392
|
-
}
|
|
393
385
|
static make(config) {
|
|
394
386
|
return new Machine(config.initial, config.state, config.event, config.guards, config.effects);
|
|
395
387
|
}
|
|
@@ -410,27 +402,36 @@ var TransitionScope = class {
|
|
|
410
402
|
};
|
|
411
403
|
const make = Machine.make;
|
|
412
404
|
/**
|
|
413
|
-
* Spawn an actor from a
|
|
405
|
+
* Spawn an actor from a machine.
|
|
406
|
+
*
|
|
407
|
+
* For machines with slots, pass implementations via `{ slots: { ... } }`.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* // No slots
|
|
412
|
+
* const actor = yield* Machine.spawn(machine);
|
|
414
413
|
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* for that state (timers, scoped resources, etc.). Transition history
|
|
420
|
-
* is not replayed — only the current state's entry effects run.
|
|
414
|
+
* // With slots
|
|
415
|
+
* const actor = yield* Machine.spawn(machine, {
|
|
416
|
+
* slots: { canRetry: ({ max }, { state }) => state.attempts < max },
|
|
417
|
+
* });
|
|
421
418
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
419
|
+
* // With hydration
|
|
420
|
+
* const actor = yield* Machine.spawn(machine, { hydrate: savedState });
|
|
421
|
+
* ```
|
|
424
422
|
*/
|
|
425
|
-
const spawn = Effect.fn("effect-machine.spawn")(function* (
|
|
423
|
+
const spawn = Effect.fn("effect-machine.spawn")(function* (machine, idOrOptions) {
|
|
426
424
|
const opts = typeof idOrOptions === "string" ? { id: idOrOptions } : idOrOptions;
|
|
427
|
-
const actor = yield* createActor(opts?.id ?? `actor-${
|
|
425
|
+
const actor = yield* createActor(opts?.id ?? `actor-${(yield* Random.next).toString(36).slice(2)}`, materializeMachine(machine, opts?.slots), {
|
|
426
|
+
initialState: opts?.hydrate,
|
|
427
|
+
supervision: opts?.supervision
|
|
428
|
+
});
|
|
428
429
|
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
429
430
|
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, actor.stop);
|
|
430
431
|
return actor;
|
|
431
432
|
});
|
|
432
|
-
const replay = Effect.fn("effect-machine.replay")(function* (
|
|
433
|
-
const machine =
|
|
433
|
+
const replay = Effect.fn("effect-machine.replay")(function* (input, events, options) {
|
|
434
|
+
const machine = materializeMachine(input, options?.slots);
|
|
434
435
|
let state = options?.from ?? machine.initial;
|
|
435
436
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
436
437
|
const postponed = [];
|
|
@@ -476,4 +477,4 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
|
|
|
476
477
|
const reply = makeReply;
|
|
477
478
|
const deferReply = makeDeferReply;
|
|
478
479
|
//#endregion
|
|
479
|
-
export {
|
|
480
|
+
export { Machine, deferReply, findTransitions, machine_exports, make, materializeMachine, replay, reply, spawn };
|
package/dist/schema.js
CHANGED
|
@@ -78,7 +78,7 @@ const buildMachineSchema = (definition) => {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
const variantArray = Object.values(variants);
|
|
81
|
-
if (variantArray.length === 0) throw new InvalidSchemaError({});
|
|
81
|
+
if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
|
|
82
82
|
const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
|
|
83
83
|
const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
|
|
84
84
|
const $match = (valueOrCases, maybeCases) => {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Cause, Duration, Schedule } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/supervision.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Where in the actor lifecycle a defect occurred.
|
|
6
|
+
*
|
|
7
|
+
* - `transition` — during event handler execution
|
|
8
|
+
* - `spawn` — during state spawn effect execution
|
|
9
|
+
* - `background` — in a background effect fiber
|
|
10
|
+
* - `initial-spawn` — during initial state spawn effects (before event loop)
|
|
11
|
+
*/
|
|
12
|
+
type DefectPhase = "transition" | "spawn" | "background" | "initial-spawn";
|
|
13
|
+
/**
|
|
14
|
+
* Terminal exit reason for an actor generation.
|
|
15
|
+
*
|
|
16
|
+
* - `Final` — machine reached a final state normally
|
|
17
|
+
* - `Stopped` — explicit `actor.stop` or `actor.drain`
|
|
18
|
+
* - `Defect` — unhandled error in the runtime
|
|
19
|
+
*/
|
|
20
|
+
type ActorExit<S> = {
|
|
21
|
+
readonly _tag: "Final";
|
|
22
|
+
readonly state: S;
|
|
23
|
+
} | {
|
|
24
|
+
readonly _tag: "Stopped";
|
|
25
|
+
} | {
|
|
26
|
+
readonly _tag: "Defect";
|
|
27
|
+
readonly cause: Cause.Cause<unknown>;
|
|
28
|
+
readonly phase: DefectPhase;
|
|
29
|
+
};
|
|
30
|
+
/** Constructors for ActorExit */
|
|
31
|
+
declare const ActorExit: {
|
|
32
|
+
readonly Final: <S>(state: S) => ActorExit<S>;
|
|
33
|
+
readonly Stopped: ActorExit<never>;
|
|
34
|
+
readonly Defect: <S = never>(cause: Cause.Cause<unknown>, phase: DefectPhase) => ActorExit<S>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Phase state for supervised actors. Serializes concurrent stop/restart/drain.
|
|
38
|
+
*
|
|
39
|
+
* Transitions:
|
|
40
|
+
* - `Running` → crash → `Restarting` → new runtime → `Running`
|
|
41
|
+
* - `Running` → explicit stop/drain → `Stopping` → `Terminated`
|
|
42
|
+
* - `Restarting` → explicit stop → `Stopping` → `Terminated`
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
type CellPhase<S> = {
|
|
47
|
+
readonly _tag: "Running";
|
|
48
|
+
readonly generation: number;
|
|
49
|
+
} | {
|
|
50
|
+
readonly _tag: "Restarting";
|
|
51
|
+
readonly generation: number;
|
|
52
|
+
} | {
|
|
53
|
+
readonly _tag: "Stopping";
|
|
54
|
+
} | {
|
|
55
|
+
readonly _tag: "Terminated";
|
|
56
|
+
readonly exit: ActorExit<S>;
|
|
57
|
+
};
|
|
58
|
+
declare namespace Supervision {
|
|
59
|
+
/**
|
|
60
|
+
* Supervision policy for actor restart behavior.
|
|
61
|
+
*
|
|
62
|
+
* `schedule` controls restart timing and budget — schedule exhaustion means terminal stop.
|
|
63
|
+
* `shouldRestart` optionally classifies defects — return `false` to stop immediately
|
|
64
|
+
* without consuming the schedule.
|
|
65
|
+
*/
|
|
66
|
+
interface Policy {
|
|
67
|
+
/** Schedule that controls restart timing. Exhaustion = terminal stop. */
|
|
68
|
+
readonly schedule: Schedule.Schedule<unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Optional classifier: given a defect exit, decide whether to restart or stop immediately.
|
|
71
|
+
* Default: always restart (let schedule handle budget).
|
|
72
|
+
*/
|
|
73
|
+
readonly shouldRestart?: (exit: Extract<ActorExit<unknown>, {
|
|
74
|
+
readonly _tag: "Defect";
|
|
75
|
+
}>) => boolean;
|
|
76
|
+
}
|
|
77
|
+
/** No supervision — crashes are terminal. */
|
|
78
|
+
const none: Policy;
|
|
79
|
+
/**
|
|
80
|
+
* Restart on defect with max restarts within a window, optional backoff.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* Supervision.restart() // unlimited restarts, no backoff
|
|
85
|
+
* Supervision.restart({ maxRestarts: 3 }) // 3 restarts then terminal
|
|
86
|
+
* Supervision.restart({ maxRestarts: 3, within: "1 minute" }) // 3 within 1 min
|
|
87
|
+
* Supervision.restart({ backoff: Schedule.exponential("100 millis") })
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
const restart: (options?: {
|
|
91
|
+
readonly maxRestarts?: number;
|
|
92
|
+
readonly within?: Duration.Input;
|
|
93
|
+
readonly backoff?: Schedule.Schedule<unknown>;
|
|
94
|
+
}) => Policy;
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { ActorExit, CellPhase, DefectPhase, Supervision };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Schedule } from "effect";
|
|
2
|
+
//#region src/supervision.ts
|
|
3
|
+
/**
|
|
4
|
+
* Supervision types for actor lifecycle management.
|
|
5
|
+
*
|
|
6
|
+
* Core concepts:
|
|
7
|
+
* - `ActorExit<S>` — why an actor stopped (final, explicit stop, or defect)
|
|
8
|
+
* - `DefectPhase` — where in the lifecycle a defect occurred
|
|
9
|
+
* - `Supervision.Policy` — Schedule-based restart policy
|
|
10
|
+
* - `CellPhase<S>` — internal phase machine for serializing stop/restart/drain
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/** Constructors for ActorExit */
|
|
15
|
+
const ActorExit = {
|
|
16
|
+
Final: (state) => ({
|
|
17
|
+
_tag: "Final",
|
|
18
|
+
state
|
|
19
|
+
}),
|
|
20
|
+
Stopped: { _tag: "Stopped" },
|
|
21
|
+
Defect: (cause, phase) => ({
|
|
22
|
+
_tag: "Defect",
|
|
23
|
+
cause,
|
|
24
|
+
phase
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
let Supervision;
|
|
28
|
+
(function(_Supervision) {
|
|
29
|
+
_Supervision.none = { schedule: Schedule.recurs(0) };
|
|
30
|
+
_Supervision.restart = (options) => {
|
|
31
|
+
let schedule = Schedule.forever;
|
|
32
|
+
if (options?.maxRestarts !== void 0) {
|
|
33
|
+
const recurs = Schedule.recurs(options.maxRestarts);
|
|
34
|
+
if (options.within !== void 0) schedule = Schedule.both(recurs, Schedule.windowed(options.within));
|
|
35
|
+
else schedule = recurs;
|
|
36
|
+
}
|
|
37
|
+
if (options?.backoff !== void 0) schedule = Schedule.both(schedule, options.backoff);
|
|
38
|
+
return { schedule };
|
|
39
|
+
};
|
|
40
|
+
})(Supervision || (Supervision = {}));
|
|
41
|
+
//#endregion
|
|
42
|
+
export { ActorExit, Supervision };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { EffectsDef, GuardsDef, MachineContext } from "./slot.js";
|
|
2
1
|
import { AssertionError } from "./errors.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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[]
|
|
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
|
|
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[]
|
|
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
|
|
94
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(input: MachineInput<S, E, R, GD, EFD>, events: readonly E[], forbiddenTag: string, options?: {
|
|
95
|
+
slots?: Record<string, any>;
|
|
96
|
+
} | undefined) => Effect.Effect<{
|
|
90
97
|
states: S[];
|
|
91
98
|
finalState: S;
|
|
92
99
|
}, AssertionError, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
@@ -107,6 +114,8 @@ interface TestHarnessOptions<S, E> {
|
|
|
107
114
|
* Useful for logging or spying on transitions.
|
|
108
115
|
*/
|
|
109
116
|
readonly onTransition?: (from: S, event: E, to: S) => void;
|
|
117
|
+
/** Slot handler implementations for machines with guards/effects. */
|
|
118
|
+
readonly slots?: Record<string, any>;
|
|
110
119
|
}
|
|
111
120
|
/**
|
|
112
121
|
* Create a test harness for step-by-step testing.
|
package/dist/testing.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { stubSystem } from "./internal/utils.js";
|
|
2
|
-
import { AssertionError } from "./errors.js";
|
|
3
|
-
import { BuiltMachine } from "./machine.js";
|
|
4
2
|
import { executeTransition, shouldPostpone } from "./internal/transition.js";
|
|
3
|
+
import { AssertionError } from "./errors.js";
|
|
4
|
+
import { materializeMachine } from "./machine.js";
|
|
5
5
|
import { Effect, SubscriptionRef } from "effect";
|
|
6
6
|
//#region src/testing.ts
|
|
7
|
+
const makeDummySelf = (label) => {
|
|
8
|
+
const dummySend = Effect.fn(label)((_event) => Effect.void);
|
|
9
|
+
return {
|
|
10
|
+
send: dummySend,
|
|
11
|
+
cast: dummySend,
|
|
12
|
+
spawn: () => Effect.die(`spawn not supported in ${label}`),
|
|
13
|
+
reply: () => Effect.succeed(false)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
7
16
|
/**
|
|
8
17
|
* Simulate a sequence of events through a machine without running an actor.
|
|
9
18
|
* Useful for testing state transitions in isolation.
|
|
@@ -24,15 +33,9 @@ import { Effect, SubscriptionRef } from "effect";
|
|
|
24
33
|
* expect(result.states).toHaveLength(3) // Idle -> Loading -> Success
|
|
25
34
|
* ```
|
|
26
35
|
*/
|
|
27
|
-
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events) {
|
|
28
|
-
const machine = input
|
|
29
|
-
const
|
|
30
|
-
const dummySelf = {
|
|
31
|
-
send: dummySend,
|
|
32
|
-
cast: dummySend,
|
|
33
|
-
spawn: () => Effect.die("spawn not supported in simulation"),
|
|
34
|
-
reply: () => Effect.succeed(false)
|
|
35
|
-
};
|
|
36
|
+
const simulate = Effect.fn("effect-machine.simulate")(function* (input, events, options) {
|
|
37
|
+
const machine = materializeMachine(input, options?.slots);
|
|
38
|
+
const dummySelf = makeDummySelf("effect-machine.testing.simulate");
|
|
36
39
|
let currentState = machine.initial;
|
|
37
40
|
const states = [currentState];
|
|
38
41
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
@@ -74,8 +77,8 @@ const simulate = Effect.fn("effect-machine.simulate")(function* (input, events)
|
|
|
74
77
|
/**
|
|
75
78
|
* Assert that a machine can reach a specific state given a sequence of events
|
|
76
79
|
*/
|
|
77
|
-
const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag) {
|
|
78
|
-
const result = yield* simulate(input, events);
|
|
80
|
+
const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input, events, expectedTag, options) {
|
|
81
|
+
const result = yield* simulate(input, events, options);
|
|
79
82
|
if (result.finalState._tag !== expectedTag) return yield* new AssertionError({ message: `Expected final state "${expectedTag}" but got "${result.finalState._tag}". States visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
|
|
80
83
|
return result.finalState;
|
|
81
84
|
});
|
|
@@ -91,8 +94,8 @@ const assertReaches = Effect.fn("effect-machine.assertReaches")(function* (input
|
|
|
91
94
|
* )
|
|
92
95
|
* ```
|
|
93
96
|
*/
|
|
94
|
-
const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath) {
|
|
95
|
-
const result = yield* simulate(input, events);
|
|
97
|
+
const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, events, expectedPath, options) {
|
|
98
|
+
const result = yield* simulate(input, events, options);
|
|
96
99
|
const actualPath = result.states.map((s) => s._tag);
|
|
97
100
|
if (actualPath.length !== expectedPath.length) return yield* new AssertionError({ message: `Path length mismatch. Expected ${expectedPath.length} states but got ${actualPath.length}.\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
|
|
98
101
|
for (let i = 0; i < expectedPath.length; i++) if (actualPath[i] !== expectedPath[i]) return yield* new AssertionError({ message: `Path mismatch at position ${i}. Expected "${expectedPath[i]}" but got "${actualPath[i]}".\nExpected: ${expectedPath.join(" -> ")}\nActual: ${actualPath.join(" -> ")}` });
|
|
@@ -111,8 +114,8 @@ const assertPath = Effect.fn("effect-machine.assertPath")(function* (input, even
|
|
|
111
114
|
* )
|
|
112
115
|
* ```
|
|
113
116
|
*/
|
|
114
|
-
const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag) {
|
|
115
|
-
const result = yield* simulate(input, events);
|
|
117
|
+
const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(function* (input, events, forbiddenTag, options) {
|
|
118
|
+
const result = yield* simulate(input, events, options);
|
|
116
119
|
const visitedIndex = result.states.findIndex((s) => s._tag === forbiddenTag);
|
|
117
120
|
if (visitedIndex !== -1) return yield* new AssertionError({ message: `Machine reached forbidden state "${forbiddenTag}" at position ${visitedIndex}.\nStates visited: ${result.states.map((s) => s._tag).join(" -> ")}` });
|
|
118
121
|
return result;
|
|
@@ -139,14 +142,8 @@ const assertNeverReaches = Effect.fn("effect-machine.assertNeverReaches")(functi
|
|
|
139
142
|
* ```
|
|
140
143
|
*/
|
|
141
144
|
const createTestHarness = Effect.fn("effect-machine.createTestHarness")(function* (input, options) {
|
|
142
|
-
const machine = input
|
|
143
|
-
const
|
|
144
|
-
const dummySelf = {
|
|
145
|
-
send: dummySend,
|
|
146
|
-
cast: dummySend,
|
|
147
|
-
spawn: () => Effect.die("spawn not supported in test harness"),
|
|
148
|
-
reply: () => Effect.succeed(false)
|
|
149
|
-
};
|
|
145
|
+
const machine = materializeMachine(input, options?.slots);
|
|
146
|
+
const dummySelf = makeDummySelf("effect-machine.testing.harness");
|
|
150
147
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
151
148
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
152
149
|
const postponed = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-machine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/cevr/effect-machine.git"
|
|
@@ -61,15 +61,15 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@changesets/changelog-github": "^0.6.0",
|
|
63
63
|
"@changesets/cli": "^2.30.0",
|
|
64
|
-
"@effect/language-service": "^0.
|
|
64
|
+
"@effect/language-service": "^0.84.2",
|
|
65
65
|
"@types/bun": "1.3.11",
|
|
66
66
|
"concurrently": "^9.2.1",
|
|
67
67
|
"effect-bun-test": "0.3.0",
|
|
68
68
|
"effect-v3": "npm:effect@^3.21.0",
|
|
69
69
|
"lefthook": "^2.1.4",
|
|
70
|
-
"oxfmt": "^0.
|
|
71
|
-
"oxlint": "^1.
|
|
72
|
-
"tsdown": "^0.21.
|
|
70
|
+
"oxfmt": "^0.42.0",
|
|
71
|
+
"oxlint": "^1.57.0",
|
|
72
|
+
"tsdown": "^0.21.7",
|
|
73
73
|
"typescript": "^5.9.3"
|
|
74
74
|
},
|
|
75
75
|
"peerDependencies": {
|