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