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