effect-machine 0.11.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 +52 -31
- package/dist/actor.js +218 -283
- package/dist/cluster/adapters/in-memory.d.ts +28 -0
- package/dist/cluster/adapters/in-memory.js +79 -0
- package/dist/cluster/entity-actor-ref.d.ts +56 -0
- package/dist/cluster/entity-actor-ref.js +33 -0
- package/dist/cluster/entity-machine.d.ts +31 -49
- package/dist/cluster/entity-machine.js +178 -52
- package/dist/cluster/index.d.ts +5 -2
- package/dist/cluster/index.js +4 -1
- package/dist/cluster/persistence.d.ts +49 -0
- package/dist/cluster/persistence.js +18 -0
- package/dist/cluster/to-entity.d.ts +9 -3
- package/dist/cluster/to-entity.js +16 -4
- package/dist/errors.d.ts +25 -17
- package/dist/errors.js +10 -5
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +142 -0
- package/dist/internal/runtime.js +357 -0
- package/dist/internal/transition.d.ts +10 -4
- package/dist/internal/transition.js +24 -12
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +89 -55
- package/dist/machine.js +80 -68
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +33 -4
- 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 -23
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +54 -37
- package/v3/dist/actor.js +209 -277
- package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
- package/v3/dist/cluster/adapters/in-memory.js +62 -0
- package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
- package/v3/dist/cluster/entity-actor-ref.js +19 -0
- package/v3/dist/cluster/entity-machine.d.ts +34 -49
- package/v3/dist/cluster/entity-machine.js +134 -50
- package/v3/dist/cluster/index.d.ts +5 -2
- package/v3/dist/cluster/index.js +4 -1
- package/v3/dist/cluster/persistence.d.ts +48 -0
- package/v3/dist/cluster/persistence.js +14 -0
- package/v3/dist/cluster/to-entity.d.ts +5 -2
- package/v3/dist/cluster/to-entity.js +12 -4
- package/v3/dist/errors.d.ts +18 -8
- package/v3/dist/errors.js +9 -4
- package/v3/dist/index.d.ts +6 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +142 -0
- package/v3/dist/internal/runtime.js +335 -0
- package/v3/dist/internal/transition.d.ts +10 -4
- package/v3/dist/internal/transition.js +23 -11
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +35 -47
- package/v3/dist/machine.js +62 -64
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
- 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/schema.js
CHANGED
|
@@ -38,6 +38,7 @@ import { Schema } from "effect";
|
|
|
38
38
|
*
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
41
|
+
const ReplySchemaSymbol = Symbol.for("effect-machine/ReplySchema");
|
|
41
42
|
/**
|
|
42
43
|
* Build a schema-first definition from a record of tag -> fields
|
|
43
44
|
*/
|
|
@@ -45,9 +46,14 @@ const RESERVED_DERIVE_KEYS = new Set(["_tag"]);
|
|
|
45
46
|
const buildMachineSchema = (definition) => {
|
|
46
47
|
const variants = {};
|
|
47
48
|
const constructors = {};
|
|
49
|
+
const replySchemas = /* @__PURE__ */ new Map();
|
|
48
50
|
for (const tag of Object.keys(definition)) {
|
|
49
51
|
const fields = definition[tag];
|
|
50
52
|
if (fields === void 0) continue;
|
|
53
|
+
if (ReplySchemaSymbol in fields) {
|
|
54
|
+
const rs = fields[ReplySchemaSymbol];
|
|
55
|
+
if (rs !== void 0) replySchemas.set(tag, rs);
|
|
56
|
+
}
|
|
51
57
|
variants[tag] = Schema.TaggedStruct(tag, fields);
|
|
52
58
|
const fieldNames = new Set(Object.keys(fields));
|
|
53
59
|
if (fieldNames.size > 0) {
|
|
@@ -72,7 +78,7 @@ const buildMachineSchema = (definition) => {
|
|
|
72
78
|
};
|
|
73
79
|
}
|
|
74
80
|
const variantArray = Object.values(variants);
|
|
75
|
-
if (variantArray.length === 0) throw new InvalidSchemaError({});
|
|
81
|
+
if (variantArray.length === 0) throw new InvalidSchemaError({ message: "Schema must have at least one variant" });
|
|
76
82
|
const unionSchema = variantArray.length === 1 ? variantArray[0] : Schema.Union(variantArray);
|
|
77
83
|
const $is = (tag) => (u) => typeof u === "object" && u !== null && "_tag" in u && u._tag === tag;
|
|
78
84
|
const $match = (valueOrCases, maybeCases) => {
|
|
@@ -94,6 +100,7 @@ const buildMachineSchema = (definition) => {
|
|
|
94
100
|
variants,
|
|
95
101
|
constructors,
|
|
96
102
|
_definition: definition,
|
|
103
|
+
replySchemas,
|
|
97
104
|
$is,
|
|
98
105
|
$match
|
|
99
106
|
};
|
|
@@ -103,10 +110,11 @@ const buildMachineSchema = (definition) => {
|
|
|
103
110
|
* Builds the schema object with variants, constructors, $is, and $match.
|
|
104
111
|
*/
|
|
105
112
|
const createMachineSchema = (definition) => {
|
|
106
|
-
const { schema, variants, constructors, _definition, $is, $match } = buildMachineSchema(definition);
|
|
113
|
+
const { schema, variants, constructors, _definition, replySchemas, $is, $match } = buildMachineSchema(definition);
|
|
107
114
|
return Object.assign(Object.create(schema), {
|
|
108
115
|
variants,
|
|
109
116
|
_definition,
|
|
117
|
+
_replySchemas: replySchemas,
|
|
110
118
|
$is,
|
|
111
119
|
$match,
|
|
112
120
|
...constructors
|
|
@@ -149,19 +157,40 @@ const State = (definition) => createMachineSchema(definition);
|
|
|
149
157
|
* accidental use of constructors from different event schemas
|
|
150
158
|
* (unless they have identical definitions).
|
|
151
159
|
*
|
|
160
|
+
* Use `Event.reply(fields, replySchema)` to define events that support
|
|
161
|
+
* typed `ask()` replies.
|
|
162
|
+
*
|
|
152
163
|
* @example
|
|
153
164
|
* ```ts
|
|
154
|
-
* const OrderEvent =
|
|
165
|
+
* const OrderEvent = Event({
|
|
155
166
|
* Ship: { trackingId: Schema.String },
|
|
156
167
|
* Cancel: {},
|
|
168
|
+
* GetTotal: Event.reply({}, Schema.Number),
|
|
157
169
|
* })
|
|
158
170
|
*
|
|
159
171
|
* type OrderEvent = typeof OrderEvent.Type
|
|
160
172
|
*
|
|
161
173
|
* // Construct
|
|
162
174
|
* const e = OrderEvent.Ship({ trackingId: "abc" })
|
|
175
|
+
*
|
|
176
|
+
* // Typed ask
|
|
177
|
+
* const total = yield* actor.ask(OrderEvent.GetTotal) // number
|
|
163
178
|
* ```
|
|
164
179
|
*/
|
|
165
|
-
const
|
|
180
|
+
const EventImpl = (definition) => createMachineSchema(definition);
|
|
181
|
+
/**
|
|
182
|
+
* Annotate event fields with a reply schema.
|
|
183
|
+
* Events defined with `Event.reply(fields, replySchema)` enable typed `ask()`.
|
|
184
|
+
*/
|
|
185
|
+
const replyFieldsFn = (fields, replySchema) => {
|
|
186
|
+
const annotated = { ...fields };
|
|
187
|
+
Object.defineProperty(annotated, ReplySchemaSymbol, {
|
|
188
|
+
value: replySchema,
|
|
189
|
+
enumerable: false,
|
|
190
|
+
writable: false
|
|
191
|
+
});
|
|
192
|
+
return annotated;
|
|
193
|
+
};
|
|
194
|
+
const Event = Object.assign(EventImpl, { reply: replyFieldsFn });
|
|
166
195
|
//#endregion
|
|
167
196
|
export { Event, State };
|
|
@@ -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,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
|
|
29
|
-
const
|
|
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("effect-machine.testing.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
|
|
142
|
-
const
|
|
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("effect-machine.testing.harness");
|
|
148
147
|
const stateRef = yield* SubscriptionRef.make(machine.initial);
|
|
149
148
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
150
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"
|
|
@@ -56,20 +56,20 @@
|
|
|
56
56
|
"release": "bun run build && changeset publish"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"effect": "4.0.0-beta.
|
|
59
|
+
"effect": "4.0.0-beta.42"
|
|
60
60
|
},
|
|
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": {
|
|
@@ -81,6 +81,6 @@
|
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"overrides": {
|
|
84
|
-
"effect": "4.0.0-beta.
|
|
84
|
+
"effect": "4.0.0-beta.42"
|
|
85
85
|
}
|
|
86
86
|
}
|
package/v3/dist/actor.d.ts
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ActorExit, Supervision } from "./supervision.js";
|
|
2
|
+
import { ExtractReply, ReplyTypeBrand } from "./internal/brands.js";
|
|
2
3
|
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
4
|
+
import { EffectsDef, GuardsDef } from "./slot.js";
|
|
3
5
|
import { ProcessEventError, ProcessEventHooks, ProcessEventResult, processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
4
|
-
import {
|
|
6
|
+
import { Machine } from "./machine.js";
|
|
7
|
+
import { RuntimeQueuedEvent } from "./internal/runtime.js";
|
|
5
8
|
import { Context, Deferred, Effect, Layer, Option, PubSub, Queue, Ref, Scope, Stream, SubscriptionRef } from "effect";
|
|
6
|
-
import * as effect_dist_dts_Tracer_js0 from "effect/dist/dts/Tracer.js";
|
|
7
9
|
|
|
8
10
|
//#region src/actor.d.ts
|
|
9
|
-
/** Discriminated mailbox request */
|
|
10
|
-
type QueuedEvent<E> =
|
|
11
|
-
readonly _tag: "send";
|
|
12
|
-
readonly event: E;
|
|
13
|
-
} | {
|
|
14
|
-
readonly _tag: "call";
|
|
15
|
-
readonly event: E;
|
|
16
|
-
readonly reply: Deferred.Deferred<ProcessEventResult<{
|
|
17
|
-
readonly _tag: string;
|
|
18
|
-
}>, ActorStoppedError>;
|
|
19
|
-
} | {
|
|
20
|
-
readonly _tag: "ask";
|
|
21
|
-
readonly event: E;
|
|
22
|
-
readonly reply: Deferred.Deferred<unknown, NoReplyError | ActorStoppedError>;
|
|
23
|
-
};
|
|
24
|
-
/**
|
|
25
|
-
* Reference to a running actor.
|
|
26
|
-
*/
|
|
11
|
+
/** Discriminated mailbox request — alias for RuntimeQueuedEvent */
|
|
12
|
+
type QueuedEvent<E> = RuntimeQueuedEvent<E>;
|
|
27
13
|
/**
|
|
28
14
|
* Sync projection of ActorRef for non-Effect boundaries (React hooks, framework callbacks).
|
|
29
15
|
*/
|
|
@@ -59,11 +45,11 @@ interface ActorRef<State extends {
|
|
|
59
45
|
*/
|
|
60
46
|
readonly call: (event: Event) => Effect.Effect<ProcessEventResult<State>>;
|
|
61
47
|
/**
|
|
62
|
-
* Typed request-reply.
|
|
63
|
-
*
|
|
48
|
+
* Typed request-reply. Accepts only events with a reply schema
|
|
49
|
+
* (defined via `Event.reply()`). Return type is inferred from the schema.
|
|
64
50
|
* Fails with NoReplyError if the handler doesn't provide a reply.
|
|
65
51
|
*/
|
|
66
|
-
readonly ask: <
|
|
52
|
+
readonly ask: <E extends Event & ReplyTypeBrand<unknown>>(event: E) => Effect.Effect<ExtractReply<E>, NoReplyError | ActorStoppedError>;
|
|
67
53
|
/** Observable state. */
|
|
68
54
|
readonly state: SubscriptionRef.SubscriptionRef<State>;
|
|
69
55
|
/** Stop the actor gracefully. */
|
|
@@ -103,6 +89,25 @@ interface ActorRef<State extends {
|
|
|
103
89
|
};
|
|
104
90
|
/** Subscribe to state changes (sync callback). Returns unsubscribe function. */
|
|
105
91
|
readonly subscribe: (fn: (state: State) => void) => () => void;
|
|
92
|
+
/**
|
|
93
|
+
* Wait for this actor's terminal exit. Resolves with the exit reason.
|
|
94
|
+
* Set exactly once when the actor terminates (final, stop, drain, or defect).
|
|
95
|
+
*/
|
|
96
|
+
readonly awaitExit: Effect.Effect<ActorExit<State>>;
|
|
97
|
+
/**
|
|
98
|
+
* Watch another actor. Returns an Effect that resolves with the exit reason
|
|
99
|
+
* when the watched actor terminally stops. Ignores restarts.
|
|
100
|
+
* Built on the other actor's exitDeferred — authoritative, not system events.
|
|
101
|
+
*/
|
|
102
|
+
readonly watch: (other: {
|
|
103
|
+
readonly id: string;
|
|
104
|
+
readonly awaitExit: Effect.Effect<ActorExit<unknown>>;
|
|
105
|
+
}) => Effect.Effect<ActorExit<unknown>>;
|
|
106
|
+
/**
|
|
107
|
+
* Drain: process all remaining events in the queue, then stop.
|
|
108
|
+
* Unlike `stop` (which interrupts immediately), `drain` lets the actor finish its work.
|
|
109
|
+
*/
|
|
110
|
+
readonly drain: Effect.Effect<void>;
|
|
106
111
|
/** Sync helpers for non-Effect boundaries. */
|
|
107
112
|
readonly sync: ActorRefSync<State, Event>;
|
|
108
113
|
/** The actor system this actor belongs to. */
|
|
@@ -121,10 +126,17 @@ type SystemEvent = {
|
|
|
121
126
|
readonly _tag: "ActorSpawned";
|
|
122
127
|
readonly id: string;
|
|
123
128
|
readonly actor: ActorRef<AnyState, unknown>;
|
|
129
|
+
} | {
|
|
130
|
+
readonly _tag: "ActorRestarted";
|
|
131
|
+
readonly id: string;
|
|
132
|
+
readonly actor: ActorRef<AnyState, unknown>;
|
|
133
|
+
readonly generation: number;
|
|
134
|
+
readonly exit: ActorExit<unknown>;
|
|
124
135
|
} | {
|
|
125
136
|
readonly _tag: "ActorStopped";
|
|
126
137
|
readonly id: string;
|
|
127
138
|
readonly actor: ActorRef<AnyState, unknown>;
|
|
139
|
+
readonly exit: ActorExit<unknown>;
|
|
128
140
|
};
|
|
129
141
|
/**
|
|
130
142
|
* Listener callback for system events.
|
|
@@ -136,18 +148,15 @@ type SystemEventListener = (event: SystemEvent) => void;
|
|
|
136
148
|
interface ActorSystem {
|
|
137
149
|
/**
|
|
138
150
|
* Spawn a new actor with the given machine.
|
|
139
|
-
*
|
|
140
|
-
* @example
|
|
141
|
-
* ```ts
|
|
142
|
-
* const built = machine.build({ fetchData: ... })
|
|
143
|
-
* const actor = yield* system.spawn("my-actor", built);
|
|
144
|
-
* ```
|
|
145
151
|
*/
|
|
146
152
|
readonly spawn: <S extends {
|
|
147
153
|
readonly _tag: string;
|
|
148
154
|
}, E extends {
|
|
149
155
|
readonly _tag: string;
|
|
150
|
-
}, R>(id: string, machine:
|
|
156
|
+
}, R>(id: string, machine: Machine<S, E, R, any, any, any, any>, options?: {
|
|
157
|
+
slots?: Record<string, any>;
|
|
158
|
+
supervision?: Supervision.Policy;
|
|
159
|
+
}) => Effect.Effect<ActorRef<S, E>, DuplicateActorError, R>;
|
|
151
160
|
/**
|
|
152
161
|
* Get an existing actor by ID
|
|
153
162
|
*/
|
|
@@ -183,15 +192,16 @@ type Listeners<S> = Set<(state: S) => void>;
|
|
|
183
192
|
*/
|
|
184
193
|
declare const notifyListeners: <S>(listeners: Listeners<S>, state: S) => void;
|
|
185
194
|
/**
|
|
186
|
-
* Build core ActorRef methods
|
|
195
|
+
* Build core ActorRef methods.
|
|
187
196
|
*/
|
|
188
197
|
declare const buildActorRefCore: <S extends {
|
|
189
198
|
readonly _tag: string;
|
|
190
199
|
}, E extends {
|
|
191
200
|
readonly _tag: string;
|
|
192
|
-
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>,
|
|
201
|
+
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, any, any, GD, EFD>, stateRef: SubscriptionRef.SubscriptionRef<S>, eventQueueRef: Ref.Ref<Queue.Queue<QueuedEvent<E>>>, stoppedRef: Ref.Ref<boolean>, listeners: Listeners<S>, stop: Effect.Effect<void>, system: ActorSystem, childrenMap: ReadonlyMap<string, ActorRef<AnyState, unknown>>, pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, transitionsPubSub: PubSub.PubSub<TransitionInfo<S, E>> | undefined, exitDeferred: Deferred.Deferred<ActorExit<S>, never>) => ActorRef<S, E>;
|
|
193
202
|
/**
|
|
194
|
-
* Create and start an actor for a machine
|
|
203
|
+
* Create and start an actor for a machine.
|
|
204
|
+
* Uses the shared runtime kernel with lifecycle hooks for actor-specific concerns.
|
|
195
205
|
*/
|
|
196
206
|
declare const createActor: <S extends {
|
|
197
207
|
readonly _tag: string;
|
|
@@ -199,12 +209,19 @@ declare const createActor: <S extends {
|
|
|
199
209
|
readonly _tag: string;
|
|
200
210
|
}, R, GD extends GuardsDef, EFD extends EffectsDef>(id: string, machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, options?: {
|
|
201
211
|
initialState?: S;
|
|
202
|
-
|
|
212
|
+
supervision?: Supervision.Policy; /** @internal Called by system after each restart — emits ActorRestarted system event */
|
|
213
|
+
onRestart?: (generation: number, exit: ActorExit<unknown>) => Effect.Effect<void>;
|
|
214
|
+
} | undefined) => Effect.Effect<ActorRef<S, E>, never, never>;
|
|
203
215
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
204
216
|
declare const settlePendingReplies: (pendingReplies: Set<Deferred.Deferred<unknown, unknown>>, actorId: string) => Effect.Effect<void, never, never>;
|
|
217
|
+
/**
|
|
218
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
219
|
+
* @internal — use Default layer for normal usage
|
|
220
|
+
*/
|
|
221
|
+
declare const makeSystem: () => Effect.Effect<ActorSystem, never, Scope.Scope>;
|
|
205
222
|
/**
|
|
206
223
|
* Default ActorSystem layer
|
|
207
224
|
*/
|
|
208
|
-
declare const Default: Layer.Layer<ActorSystem, never,
|
|
225
|
+
declare const Default: Layer.Layer<ActorSystem, never, Scope.Scope>;
|
|
209
226
|
//#endregion
|
|
210
|
-
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
227
|
+
export { ActorRef, ActorRefSync, ActorSystem, Default, Listeners, type ProcessEventError, type ProcessEventHooks, type ProcessEventResult, QueuedEvent, SystemEvent, SystemEventListener, TransitionInfo, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|