effect-machine 0.19.0 → 0.21.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 +171 -173
- package/dist/actor.d.ts +95 -24
- package/dist/actor.js +215 -84
- package/dist/atom.d.ts +56 -3
- package/dist/atom.js +33 -2
- 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/index.js +2 -2
- 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/internal/utils.js +1 -0
- 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 +3 -2
package/dist/internal/runtime.js
CHANGED
|
@@ -2,7 +2,6 @@ import { INTERNAL_INIT_EVENT, isEffect } from "./utils.js";
|
|
|
2
2
|
import { ActorStoppedError, NoReplyError } from "../errors.js";
|
|
3
3
|
import { makeEventAdvancement } from "./event-advancement.js";
|
|
4
4
|
import { processEventCoreImmediate, runSpawnEffects, shouldPostpone } from "./transition.js";
|
|
5
|
-
import { ActorExit } from "../supervision.js";
|
|
6
5
|
import { ActorSystem } from "../actor.js";
|
|
7
6
|
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
|
|
8
7
|
//#region src/internal/runtime.ts
|
|
@@ -18,12 +17,24 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
|
|
|
18
17
|
* - Reply settlement (call/ask Deferreds)
|
|
19
18
|
* - Reply schema validation
|
|
20
19
|
* - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
|
|
21
|
-
* -
|
|
20
|
+
* - RuntimeExit with exit reason (Final/Stopped/Defect) via exitDeferred
|
|
22
21
|
*
|
|
23
22
|
* Used by entity-machine and local actor (actor.ts delegates here).
|
|
24
23
|
*
|
|
25
24
|
* @internal
|
|
26
25
|
*/
|
|
26
|
+
const RuntimeExit = {
|
|
27
|
+
Final: (state) => ({
|
|
28
|
+
_tag: "Final",
|
|
29
|
+
state
|
|
30
|
+
}),
|
|
31
|
+
Stopped: { _tag: "Stopped" },
|
|
32
|
+
Defect: (cause, phase) => ({
|
|
33
|
+
_tag: "Defect",
|
|
34
|
+
cause,
|
|
35
|
+
phase
|
|
36
|
+
})
|
|
37
|
+
};
|
|
27
38
|
/**
|
|
28
39
|
* Create a runtime for a machine. Returns a handle for sending events
|
|
29
40
|
* and querying state. The runtime owns:
|
|
@@ -41,6 +52,7 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
|
|
|
41
52
|
*/
|
|
42
53
|
const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
|
|
43
54
|
const { actorId, hooks, lifecycle } = config;
|
|
55
|
+
const generation = config.generation ?? 0;
|
|
44
56
|
const services = yield* Effect.context();
|
|
45
57
|
const fork = Effect.runForkWith(services);
|
|
46
58
|
const { stateRef, stoppedRef, eventQueue } = config.cellResources;
|
|
@@ -84,51 +96,78 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
84
96
|
yield* Deferred.await(startDeferred);
|
|
85
97
|
return;
|
|
86
98
|
}
|
|
99
|
+
const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
|
|
100
|
+
const loopFiber = loopFiberRef.current;
|
|
101
|
+
if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
|
|
102
|
+
return Effect.void;
|
|
103
|
+
})), Effect.asVoid);
|
|
104
|
+
const initialState = yield* SubscriptionRef.get(stateRef);
|
|
105
|
+
const initialProcessing = processEventCoreImmediate(machine, initialState, initEvent, self, stateScopeRef, system, actorId, {
|
|
106
|
+
...hooks,
|
|
107
|
+
onSpawnDefect: initialSpawnDefectSignal
|
|
108
|
+
}, generation);
|
|
109
|
+
let initialResult;
|
|
110
|
+
if (isEffect(initialProcessing)) initialResult = yield* initialProcessing.pipe(Effect.catchCause((cause) => Effect.gen(function* () {
|
|
111
|
+
yield* Ref.set(stoppedRef, true);
|
|
112
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
113
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
114
|
+
yield* Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "transition"));
|
|
115
|
+
return yield* Effect.failCause(cause);
|
|
116
|
+
})));
|
|
117
|
+
else initialResult = initialProcessing;
|
|
118
|
+
if (initialResult.transitioned) {
|
|
119
|
+
yield* SubscriptionRef.set(stateRef, initialResult.newState);
|
|
120
|
+
if (lifecycle?.onStateChange !== void 0) {
|
|
121
|
+
const stateChange = lifecycle.onStateChange(initialResult, initEvent);
|
|
122
|
+
if (isEffect(stateChange)) yield* stateChange;
|
|
123
|
+
}
|
|
124
|
+
if (lifecycle?.onProcessed !== void 0) {
|
|
125
|
+
const processed = lifecycle.onProcessed(initialResult, initEvent);
|
|
126
|
+
if (isEffect(processed)) yield* processed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const stableInitialState = initialResult.newState;
|
|
87
130
|
const backgroundFibers = [];
|
|
88
131
|
for (const bg of machine._backgroundEffectEntries()) {
|
|
89
132
|
const fiber = yield* bg.handler({
|
|
90
133
|
actorId,
|
|
91
|
-
|
|
134
|
+
generation,
|
|
135
|
+
state: stableInitialState,
|
|
92
136
|
event: initEvent,
|
|
93
137
|
self,
|
|
94
138
|
system
|
|
95
139
|
}).pipe(Effect.forkIn(actorScope));
|
|
96
140
|
backgroundFibers.push(fiber);
|
|
97
141
|
}
|
|
98
|
-
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(
|
|
99
|
-
|
|
100
|
-
const loopFiber = loopFiberRef.current;
|
|
101
|
-
if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
|
|
102
|
-
return Effect.void;
|
|
103
|
-
})), Effect.asVoid);
|
|
104
|
-
yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchCause((cause) => Effect.gen(function* () {
|
|
142
|
+
if (!initialResult.lifecycleRan && lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(stableInitialState);
|
|
143
|
+
if (!initialResult.lifecycleRan) yield* runSpawnEffects(machine, stableInitialState, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal, generation).pipe(Effect.catchCause((cause) => Effect.gen(function* () {
|
|
105
144
|
yield* Ref.set(stoppedRef, true);
|
|
106
145
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
107
146
|
yield* Scope.close(actorScope, Exit.void);
|
|
108
|
-
yield* Deferred.succeed(exitDeferred,
|
|
147
|
+
yield* Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "initial-spawn"));
|
|
109
148
|
return yield* Effect.failCause(cause);
|
|
110
149
|
})));
|
|
111
|
-
if (machine._isFinal(
|
|
112
|
-
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(
|
|
150
|
+
if (machine._isFinal(stableInitialState._tag)) {
|
|
151
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(stableInitialState);
|
|
113
152
|
yield* Ref.set(stoppedRef, true);
|
|
114
153
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
115
154
|
yield* Scope.close(actorScope, Exit.void);
|
|
116
|
-
yield* setExit(
|
|
155
|
+
yield* setExit(RuntimeExit.Final(stableInitialState));
|
|
117
156
|
yield* Deferred.succeed(startDeferred, void 0);
|
|
118
157
|
return;
|
|
119
158
|
}
|
|
120
159
|
const augmentedHooks = {
|
|
121
160
|
...hooks,
|
|
122
|
-
onSpawnDefect: (cause) => Deferred.succeed(exitDeferred,
|
|
161
|
+
onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, RuntimeExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => {
|
|
123
162
|
const loopFiber = loopFiberRef.current;
|
|
124
163
|
if (loopFiber !== void 0) return Fiber.interrupt(loopFiber);
|
|
125
164
|
return Effect.void;
|
|
126
165
|
})), Effect.asVoid)
|
|
127
166
|
};
|
|
128
|
-
const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
|
|
167
|
+
const loopFiber = yield* runtimeEventLoop(machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, fork).pipe(Effect.provide(services), Effect.forkDetach);
|
|
129
168
|
loopFiberRef.current = loopFiber;
|
|
130
169
|
if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
|
|
131
|
-
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) return setExit(
|
|
170
|
+
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) return setExit(RuntimeExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
|
|
132
171
|
return Effect.never;
|
|
133
172
|
})))).pipe(Effect.forkIn(actorScope));
|
|
134
173
|
yield* Effect.forkDetach(Effect.gen(function* () {
|
|
@@ -137,7 +176,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
137
176
|
else yield* Scope.close(actorScope, loopExit);
|
|
138
177
|
}));
|
|
139
178
|
yield* Deferred.succeed(startDeferred, void 0);
|
|
140
|
-
}).pipe(Effect.catchCause((cause) => Deferred.failCause(startDeferred, cause)
|
|
179
|
+
}).pipe(Effect.catchCause((cause) => Ref.set(stoppedRef, true).pipe(Effect.andThen(Scope.close(stateScopeRef.current, Exit.void)), Effect.andThen(Scope.close(actorScope, Exit.void)), Effect.andThen(setExit(RuntimeExit.Defect(cause, "transition"))), Effect.andThen(Deferred.failCause(startDeferred, cause)), Effect.andThen(Effect.failCause(cause)))));
|
|
141
180
|
const stop = Effect.gen(function* () {
|
|
142
181
|
if (yield* Ref.get(stoppedRef)) return;
|
|
143
182
|
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
@@ -147,7 +186,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
147
186
|
if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
|
|
148
187
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
149
188
|
yield* Scope.close(actorScope, Exit.void);
|
|
150
|
-
yield* setExit(
|
|
189
|
+
yield* setExit(RuntimeExit.Stopped);
|
|
151
190
|
}).pipe(Effect.asVoid);
|
|
152
191
|
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
153
192
|
return {
|
|
@@ -233,7 +272,7 @@ const settlePendingRequests = (pendingRequests, actorId) => Effect.gen(function*
|
|
|
233
272
|
for (const settle of pendingRequests) yield* settle(error);
|
|
234
273
|
pendingRequests.clear();
|
|
235
274
|
});
|
|
236
|
-
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
|
|
275
|
+
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, pendingRequests, stoppedRef, self, stateScopeRef, actorId, generation, system, exitDeferred, hooks, deferredReplyRef, lifecycle, fork) {
|
|
237
276
|
const forkEffect = fork ?? Effect.runFork;
|
|
238
277
|
/** Set the exit deferred exactly once. */
|
|
239
278
|
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
@@ -248,7 +287,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
248
287
|
hasReply: false,
|
|
249
288
|
deferReply: false,
|
|
250
289
|
reply: void 0,
|
|
251
|
-
postponed: true
|
|
290
|
+
postponed: true,
|
|
291
|
+
transitions: []
|
|
252
292
|
};
|
|
253
293
|
let input = queued;
|
|
254
294
|
if (queued._tag === "call") {
|
|
@@ -276,7 +316,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
276
316
|
const processQueued = (currentState, queued) => Effect.gen(function* () {
|
|
277
317
|
const event = queued.event;
|
|
278
318
|
if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
|
|
279
|
-
const processing = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
319
|
+
const processing = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation);
|
|
280
320
|
let result;
|
|
281
321
|
if (isEffect(processing)) result = yield* processing;
|
|
282
322
|
else result = processing;
|
|
@@ -364,7 +404,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
364
404
|
hasReply: false,
|
|
365
405
|
deferReply: false,
|
|
366
406
|
reply: void 0,
|
|
367
|
-
postponed: false
|
|
407
|
+
postponed: false,
|
|
408
|
+
transitions: []
|
|
368
409
|
}));
|
|
369
410
|
}
|
|
370
411
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
@@ -373,7 +414,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
373
414
|
while (true) {
|
|
374
415
|
const queued = yield* Queue.take(eventQueue);
|
|
375
416
|
if (queued._tag === "drain") {
|
|
376
|
-
yield* shutdown(
|
|
417
|
+
yield* shutdown(RuntimeExit.Stopped);
|
|
377
418
|
yield* Deferred.succeed(queued.done, void 0);
|
|
378
419
|
return;
|
|
379
420
|
}
|
|
@@ -382,7 +423,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
382
423
|
if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
|
|
383
424
|
else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
|
|
384
425
|
else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
|
|
385
|
-
return shutdown(
|
|
426
|
+
return shutdown(RuntimeExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
386
427
|
}));
|
|
387
428
|
let stopped;
|
|
388
429
|
if (advancement === void 0) {
|
|
@@ -392,7 +433,7 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
392
433
|
} else stopped = (yield* catchEventDefect(advancement.advance(eventQueued))).stopped;
|
|
393
434
|
if (stopped) {
|
|
394
435
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
395
|
-
yield* shutdown(
|
|
436
|
+
yield* shutdown(RuntimeExit.Final(finalState));
|
|
396
437
|
return;
|
|
397
438
|
}
|
|
398
439
|
}
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import { Machine } from "../machine.js";
|
|
2
2
|
import { Transition } from "./machine-definition.js";
|
|
3
3
|
//#region src/internal/transition.d.ts
|
|
4
|
+
interface ExecutedStep<S, E> {
|
|
5
|
+
readonly previousState: S;
|
|
6
|
+
readonly newState: S;
|
|
7
|
+
readonly event: E;
|
|
8
|
+
readonly transition: Transition<S, E, never>;
|
|
9
|
+
}
|
|
10
|
+
interface ExecutedTransition<S, E = unknown> {
|
|
11
|
+
readonly newState: S;
|
|
12
|
+
readonly transitioned: boolean;
|
|
13
|
+
readonly reenter: boolean;
|
|
14
|
+
readonly hasReply: boolean;
|
|
15
|
+
readonly deferReply: boolean;
|
|
16
|
+
readonly reply: unknown;
|
|
17
|
+
readonly transition?: Transition<S, E, never>;
|
|
18
|
+
readonly steps: ReadonlyArray<ExecutedStep<S, E>>;
|
|
19
|
+
}
|
|
20
|
+
declare const executeTransitionImmediate: <S extends {
|
|
21
|
+
readonly _tag: string;
|
|
22
|
+
}, E extends {
|
|
23
|
+
readonly _tag: string;
|
|
24
|
+
}, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E, hooks?: ProcessEventHooks<S, E>) => ExecutedTransition<S, E> | Effect.Effect<ExecutedTransition<S, E>>;
|
|
4
25
|
/**
|
|
5
26
|
* Optional hooks for event processing inspection/tracing.
|
|
6
27
|
*/
|
|
7
28
|
interface ProcessEventHooks<S, E> {
|
|
29
|
+
/** Called after each guard candidate is evaluated. */
|
|
30
|
+
readonly onGuard?: (evaluation: GuardEvaluation<S, E>) => Effect.Effect<void>;
|
|
31
|
+
/** Called before an accepted transition handler runs. */
|
|
32
|
+
readonly onOperation?: (operation: TransitionOperation<S, E>) => Effect.Effect<void>;
|
|
8
33
|
/** Called before running spawn effects */
|
|
9
34
|
readonly onSpawnEffect?: (state: S) => Effect.Effect<void>;
|
|
10
35
|
/** Called after transition completes */
|
|
@@ -14,6 +39,17 @@ interface ProcessEventHooks<S, E> {
|
|
|
14
39
|
/** Called when a forked spawn fiber defects — signals the runtime to set exitDeferred */
|
|
15
40
|
readonly onSpawnDefect?: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
|
|
16
41
|
}
|
|
42
|
+
interface GuardEvaluation<S, E> {
|
|
43
|
+
readonly guard: string;
|
|
44
|
+
readonly state: S;
|
|
45
|
+
readonly event: E;
|
|
46
|
+
readonly result: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface TransitionOperation<S, E> {
|
|
49
|
+
readonly operation: string;
|
|
50
|
+
readonly state: S;
|
|
51
|
+
readonly event: E;
|
|
52
|
+
}
|
|
17
53
|
/**
|
|
18
54
|
* Error info for inspection hooks.
|
|
19
55
|
*/
|
|
@@ -26,7 +62,7 @@ interface ProcessEventError<S, E> {
|
|
|
26
62
|
/**
|
|
27
63
|
* Result of processing an event through the machine.
|
|
28
64
|
*/
|
|
29
|
-
interface ProcessEventResult<S> {
|
|
65
|
+
interface ProcessEventResult<S, E = unknown> {
|
|
30
66
|
/** New state after processing */
|
|
31
67
|
readonly newState: S;
|
|
32
68
|
/** Previous state before processing */
|
|
@@ -45,6 +81,12 @@ interface ProcessEventResult<S> {
|
|
|
45
81
|
readonly reply?: unknown;
|
|
46
82
|
/** Whether the event was postponed (buffered for retry after next state change) */
|
|
47
83
|
readonly postponed: boolean;
|
|
84
|
+
/** Each accepted edge in the stable macrostep. */
|
|
85
|
+
readonly transitions: ReadonlyArray<{
|
|
86
|
+
readonly previousState: S;
|
|
87
|
+
readonly newState: S;
|
|
88
|
+
readonly event: E;
|
|
89
|
+
}>;
|
|
48
90
|
}
|
|
49
91
|
/**
|
|
50
92
|
* Resolve which transition should fire for a given state and event.
|
|
@@ -54,6 +96,12 @@ declare const resolveTransition: <S extends {
|
|
|
54
96
|
readonly _tag: string;
|
|
55
97
|
}, E extends {
|
|
56
98
|
readonly _tag: string;
|
|
57
|
-
}, R>(machine: Machine<S, E, R, any, any>, currentState: S, event: E) => Transition<S, E, never> | undefined;
|
|
99
|
+
}, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => Transition<S, E, never> | undefined;
|
|
100
|
+
/** Resolve a transition with pure or Effect guards. */
|
|
101
|
+
declare const resolveTransitionEffect: <S extends {
|
|
102
|
+
readonly _tag: string;
|
|
103
|
+
}, E extends {
|
|
104
|
+
readonly _tag: string;
|
|
105
|
+
}, R>(machine: Machine<S, E, R, any, any, any, any>, currentState: S, event: E) => Effect.Effect<Transition<S, E, never> | undefined>;
|
|
58
106
|
//#endregion
|
|
59
|
-
export { ProcessEventError, ProcessEventHooks, ProcessEventResult, resolveTransition };
|
|
107
|
+
export { GuardEvaluation, ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionOperation, executeTransitionImmediate, resolveTransition, resolveTransitionEffect };
|
|
@@ -11,14 +11,21 @@ import { Cause, Effect, Exit, Scope } from "effect";
|
|
|
11
11
|
*
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
|
-
const completeTransition = (transition, resolved) => {
|
|
14
|
+
const completeTransition = (currentState, event, transition, resolved) => {
|
|
15
15
|
if (isReplyResult(resolved)) return {
|
|
16
16
|
newState: resolved.state,
|
|
17
17
|
transitioned: true,
|
|
18
18
|
reenter: transition.reenter === true,
|
|
19
19
|
hasReply: true,
|
|
20
20
|
deferReply: false,
|
|
21
|
-
reply: resolved.reply
|
|
21
|
+
reply: resolved.reply,
|
|
22
|
+
transition,
|
|
23
|
+
steps: [{
|
|
24
|
+
previousState: currentState,
|
|
25
|
+
newState: resolved.state,
|
|
26
|
+
event,
|
|
27
|
+
transition
|
|
28
|
+
}]
|
|
22
29
|
};
|
|
23
30
|
if (isDeferReplyResult(resolved)) return {
|
|
24
31
|
newState: resolved.state,
|
|
@@ -26,7 +33,14 @@ const completeTransition = (transition, resolved) => {
|
|
|
26
33
|
reenter: transition.reenter === true,
|
|
27
34
|
hasReply: false,
|
|
28
35
|
deferReply: true,
|
|
29
|
-
reply: void 0
|
|
36
|
+
reply: void 0,
|
|
37
|
+
transition,
|
|
38
|
+
steps: [{
|
|
39
|
+
previousState: currentState,
|
|
40
|
+
newState: resolved.state,
|
|
41
|
+
event,
|
|
42
|
+
transition
|
|
43
|
+
}]
|
|
30
44
|
};
|
|
31
45
|
return {
|
|
32
46
|
newState: resolved,
|
|
@@ -34,7 +48,14 @@ const completeTransition = (transition, resolved) => {
|
|
|
34
48
|
reenter: transition.reenter === true,
|
|
35
49
|
hasReply: false,
|
|
36
50
|
deferReply: false,
|
|
37
|
-
reply: void 0
|
|
51
|
+
reply: void 0,
|
|
52
|
+
transition,
|
|
53
|
+
steps: [{
|
|
54
|
+
previousState: currentState,
|
|
55
|
+
newState: resolved,
|
|
56
|
+
event,
|
|
57
|
+
transition
|
|
58
|
+
}]
|
|
38
59
|
};
|
|
39
60
|
};
|
|
40
61
|
/**
|
|
@@ -50,8 +71,10 @@ const completeTransition = (transition, resolved) => {
|
|
|
50
71
|
*/
|
|
51
72
|
const executeTransition = (machine, currentState, event) => Effect.suspend(() => {
|
|
52
73
|
const result = executeTransitionImmediate(machine, currentState, event);
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
let first;
|
|
75
|
+
if (isEffect(result)) first = result;
|
|
76
|
+
else first = Effect.succeed(result);
|
|
77
|
+
return first.pipe(Effect.flatMap((initialResult) => stabilizeTransition(machine, currentState, event, initialResult)));
|
|
55
78
|
});
|
|
56
79
|
/**
|
|
57
80
|
* Execute a transition without adding an Effect boundary for a synchronous handler.
|
|
@@ -59,29 +82,98 @@ const executeTransition = (machine, currentState, event) => Effect.suspend(() =>
|
|
|
59
82
|
*
|
|
60
83
|
* @internal
|
|
61
84
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (transition === void 0) return {
|
|
65
|
-
newState: currentState,
|
|
66
|
-
transitioned: false,
|
|
67
|
-
reenter: false,
|
|
68
|
-
hasReply: false,
|
|
69
|
-
deferReply: false,
|
|
70
|
-
reply: void 0
|
|
71
|
-
};
|
|
72
|
-
const handlerCtx = {
|
|
73
|
-
state: currentState,
|
|
74
|
-
event
|
|
75
|
-
};
|
|
76
|
-
let raw;
|
|
85
|
+
const executeTransitionCandidatesImmediate = (currentState, event, candidates, hooks) => {
|
|
86
|
+
let resolution;
|
|
77
87
|
try {
|
|
78
|
-
|
|
88
|
+
resolution = evaluateTransitions(candidates, currentState, event);
|
|
79
89
|
} catch (defect) {
|
|
80
90
|
return Effect.die(defect);
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
const executeResolved = (resolved) => {
|
|
93
|
+
const transition = resolved.transition;
|
|
94
|
+
if (transition === void 0) {
|
|
95
|
+
const unhandled = {
|
|
96
|
+
newState: currentState,
|
|
97
|
+
transitioned: false,
|
|
98
|
+
reenter: false,
|
|
99
|
+
hasReply: false,
|
|
100
|
+
deferReply: false,
|
|
101
|
+
reply: void 0,
|
|
102
|
+
transition: void 0,
|
|
103
|
+
steps: []
|
|
104
|
+
};
|
|
105
|
+
if (hooks?.onGuard === void 0 || resolved.evaluations.length === 0) return unhandled;
|
|
106
|
+
return Effect.forEach(resolved.evaluations, hooks.onGuard, { discard: true }).pipe(Effect.as(unhandled));
|
|
107
|
+
}
|
|
108
|
+
const runHandler = () => {
|
|
109
|
+
const handlerCtx = {
|
|
110
|
+
state: currentState,
|
|
111
|
+
event
|
|
112
|
+
};
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = transition.handler(handlerCtx);
|
|
116
|
+
} catch (defect) {
|
|
117
|
+
return Effect.die(defect);
|
|
118
|
+
}
|
|
119
|
+
if (isEffect(raw)) return raw.pipe(Effect.map((value) => completeTransition(currentState, event, transition, value)));
|
|
120
|
+
return completeTransition(currentState, event, transition, raw);
|
|
121
|
+
};
|
|
122
|
+
const runInspectedHandler = () => {
|
|
123
|
+
if (hooks?.onOperation === void 0) return runHandler();
|
|
124
|
+
return hooks.onOperation({
|
|
125
|
+
operation: transition.handler.name || "<inline>",
|
|
126
|
+
state: currentState,
|
|
127
|
+
event
|
|
128
|
+
}).pipe(Effect.andThen(Effect.suspend(() => {
|
|
129
|
+
const result = runHandler();
|
|
130
|
+
if (isEffect(result)) return result;
|
|
131
|
+
return Effect.succeed(result);
|
|
132
|
+
})));
|
|
133
|
+
};
|
|
134
|
+
if (hooks?.onGuard === void 0 || resolved.evaluations.length === 0) return runInspectedHandler();
|
|
135
|
+
return Effect.forEach(resolved.evaluations, hooks.onGuard, { discard: true }).pipe(Effect.andThen(Effect.suspend(() => {
|
|
136
|
+
const result = runInspectedHandler();
|
|
137
|
+
if (isEffect(result)) return result;
|
|
138
|
+
return Effect.succeed(result);
|
|
139
|
+
})));
|
|
140
|
+
};
|
|
141
|
+
if (!isEffect(resolution)) return executeResolved(resolution);
|
|
142
|
+
return resolution.pipe(Effect.flatMap((resolved) => {
|
|
143
|
+
const result = executeResolved(resolved);
|
|
144
|
+
if (isEffect(result)) return result;
|
|
145
|
+
return Effect.succeed(result);
|
|
146
|
+
}));
|
|
84
147
|
};
|
|
148
|
+
const executeTransitionImmediate = (machine, currentState, event, hooks) => executeTransitionCandidatesImmediate(currentState, event, machine._findTransitions(currentState._tag, event._tag), hooks);
|
|
149
|
+
const stabilizeTransition = (machine, initialState, event, result, hooks) => Effect.gen(function* () {
|
|
150
|
+
const steps = [...result.steps];
|
|
151
|
+
let stableState = result.newState;
|
|
152
|
+
let lifecycleRequired = result.reenter || stableState._tag !== initialState._tag;
|
|
153
|
+
let depth = 0;
|
|
154
|
+
while (true) {
|
|
155
|
+
const candidates = machine._findImmediateTransitions(stableState._tag);
|
|
156
|
+
if (candidates.length === 0) break;
|
|
157
|
+
depth += 1;
|
|
158
|
+
if (depth > 100) return yield* Effect.die(/* @__PURE__ */ new Error("Immediate transition limit exceeded. Check for an eventless loop."));
|
|
159
|
+
const immediate = executeTransitionCandidatesImmediate(stableState, event, candidates, hooks);
|
|
160
|
+
let next;
|
|
161
|
+
if (isEffect(immediate)) next = yield* immediate;
|
|
162
|
+
else next = immediate;
|
|
163
|
+
if (!next.transitioned) break;
|
|
164
|
+
if (next.reenter || next.newState._tag !== stableState._tag) lifecycleRequired = true;
|
|
165
|
+
steps.push(...next.steps);
|
|
166
|
+
stableState = next.newState;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...result,
|
|
170
|
+
newState: stableState,
|
|
171
|
+
transitioned: steps.length > 0,
|
|
172
|
+
reenter: lifecycleRequired,
|
|
173
|
+
transition: steps.at(-1)?.transition,
|
|
174
|
+
steps
|
|
175
|
+
};
|
|
176
|
+
});
|
|
85
177
|
/**
|
|
86
178
|
* Check if an event should be postponed in the current state.
|
|
87
179
|
* @internal
|
|
@@ -99,12 +191,12 @@ const shouldPostpone = (machine, stateTag, eventTag) => machine._shouldPostpone(
|
|
|
99
191
|
*
|
|
100
192
|
* @internal
|
|
101
193
|
*/
|
|
102
|
-
const processEventCore = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks) => Effect.suspend(() => {
|
|
103
|
-
const processed = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
194
|
+
const processEventCore = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation = 0) => Effect.suspend(() => {
|
|
195
|
+
const processed = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation);
|
|
104
196
|
if (isEffect(processed)) return processed;
|
|
105
197
|
return Effect.succeed(processed);
|
|
106
198
|
});
|
|
107
|
-
const completeProcessedEvent = (machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks) => {
|
|
199
|
+
const completeProcessedEvent = (machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks, generation = 0) => {
|
|
108
200
|
if (!result.transitioned) return {
|
|
109
201
|
newState: currentState,
|
|
110
202
|
previousState: currentState,
|
|
@@ -114,7 +206,8 @@ const completeProcessedEvent = (machine, currentState, event, result, self, stat
|
|
|
114
206
|
hasReply: false,
|
|
115
207
|
deferReply: false,
|
|
116
208
|
reply: void 0,
|
|
117
|
-
postponed: false
|
|
209
|
+
postponed: false,
|
|
210
|
+
transitions: []
|
|
118
211
|
};
|
|
119
212
|
const newState = result.newState;
|
|
120
213
|
const runLifecycle = newState._tag !== currentState._tag || result.reenter;
|
|
@@ -127,22 +220,59 @@ const completeProcessedEvent = (machine, currentState, event, result, self, stat
|
|
|
127
220
|
hasReply: result.hasReply,
|
|
128
221
|
deferReply: result.deferReply,
|
|
129
222
|
reply: result.reply,
|
|
130
|
-
postponed: false
|
|
223
|
+
postponed: false,
|
|
224
|
+
transitions: result.steps
|
|
131
225
|
};
|
|
132
|
-
|
|
226
|
+
const observeTransitions = hooks?.onTransition;
|
|
227
|
+
if (!runLifecycle) {
|
|
228
|
+
if (observeTransitions === void 0 || result.steps.length === 0) return processed;
|
|
229
|
+
return Effect.forEach(result.steps, (step) => observeTransitions(step.previousState, step.newState, step.event), { discard: true }).pipe(Effect.as(processed));
|
|
230
|
+
}
|
|
133
231
|
return Effect.gen(function* () {
|
|
134
232
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
135
233
|
stateScopeRef.current = yield* Scope.make();
|
|
136
|
-
if (
|
|
234
|
+
if (observeTransitions !== void 0) yield* Effect.forEach(result.steps, (step) => observeTransitions(step.previousState, step.newState, step.event), { discard: true });
|
|
137
235
|
if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
|
|
138
|
-
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
|
|
236
|
+
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect, generation);
|
|
139
237
|
return processed;
|
|
140
238
|
});
|
|
141
239
|
};
|
|
142
240
|
/** @internal */
|
|
143
|
-
const processEventCoreImmediate = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks) => {
|
|
144
|
-
const execution = executeTransitionImmediate(machine, currentState, event);
|
|
145
|
-
const complete = (result) =>
|
|
241
|
+
const processEventCoreImmediate = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation = 0) => {
|
|
242
|
+
const execution = executeTransitionImmediate(machine, currentState, event, hooks);
|
|
243
|
+
const complete = (result) => {
|
|
244
|
+
if (machine._findImmediateTransitions(result.newState._tag).length === 0) return completeProcessedEvent(machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks, generation);
|
|
245
|
+
return Effect.gen(function* () {
|
|
246
|
+
const steps = [...result.steps];
|
|
247
|
+
let stableState = result.newState;
|
|
248
|
+
let lifecycleRequired = result.reenter || stableState._tag !== currentState._tag;
|
|
249
|
+
let depth = 0;
|
|
250
|
+
while (true) {
|
|
251
|
+
const candidates = machine._findImmediateTransitions(stableState._tag);
|
|
252
|
+
if (candidates.length === 0) break;
|
|
253
|
+
depth += 1;
|
|
254
|
+
if (depth > 100) return yield* Effect.die(/* @__PURE__ */ new Error("Immediate transition limit exceeded. Check for an eventless loop."));
|
|
255
|
+
const immediate = executeTransitionCandidatesImmediate(stableState, event, candidates, hooks);
|
|
256
|
+
let next;
|
|
257
|
+
if (isEffect(immediate)) next = yield* immediate;
|
|
258
|
+
else next = immediate;
|
|
259
|
+
if (!next.transitioned) break;
|
|
260
|
+
if (next.reenter || next.newState._tag !== stableState._tag) lifecycleRequired = true;
|
|
261
|
+
steps.push(...next.steps);
|
|
262
|
+
stableState = next.newState;
|
|
263
|
+
}
|
|
264
|
+
const processed = completeProcessedEvent(machine, currentState, event, {
|
|
265
|
+
...result,
|
|
266
|
+
newState: stableState,
|
|
267
|
+
transitioned: steps.length > 0,
|
|
268
|
+
reenter: lifecycleRequired,
|
|
269
|
+
transition: steps.at(-1)?.transition,
|
|
270
|
+
steps
|
|
271
|
+
}, self, stateScopeRef, system, actorId, hooks, generation);
|
|
272
|
+
if (isEffect(processed)) return yield* processed;
|
|
273
|
+
return processed;
|
|
274
|
+
});
|
|
275
|
+
};
|
|
146
276
|
if (!isEffect(execution)) return complete(execution);
|
|
147
277
|
return execution.pipe(Effect.catchCause((cause) => {
|
|
148
278
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
@@ -165,13 +295,14 @@ const processEventCoreImmediate = (machine, currentState, event, self, stateScop
|
|
|
165
295
|
*
|
|
166
296
|
* @internal
|
|
167
297
|
*/
|
|
168
|
-
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
|
|
298
|
+
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect, generation = 0) {
|
|
169
299
|
const spawnEffects = machine._findSpawnEffects(state._tag);
|
|
170
300
|
const reportError = onError;
|
|
171
301
|
const defectSignal = onSpawnDefect;
|
|
172
302
|
for (const spawnEffect of spawnEffects) {
|
|
173
303
|
const effect = spawnEffect.handler({
|
|
174
304
|
actorId,
|
|
305
|
+
generation,
|
|
175
306
|
state,
|
|
176
307
|
event,
|
|
177
308
|
self,
|
|
@@ -197,7 +328,54 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
197
328
|
* Uses indexed O(1) lookup. First matching transition wins.
|
|
198
329
|
*/
|
|
199
330
|
const resolveTransition = (machine, currentState, event) => {
|
|
200
|
-
|
|
331
|
+
const resolution = evaluateTransitions(machine._findTransitions(currentState._tag, event._tag), currentState, event);
|
|
332
|
+
if (isEffect(resolution)) return Effect.runSync(Effect.die("Effect guards require actor.can(event). actor.client.canSync(event) is synchronous."));
|
|
333
|
+
return resolution.transition;
|
|
334
|
+
};
|
|
335
|
+
/** Resolve a transition with pure or Effect guards. */
|
|
336
|
+
const resolveTransitionEffect = (machine, currentState, event) => {
|
|
337
|
+
const resolution = evaluateTransitions(machine._findTransitions(currentState._tag, event._tag), currentState, event);
|
|
338
|
+
if (isEffect(resolution)) return resolution.pipe(Effect.map((value) => value.transition));
|
|
339
|
+
return Effect.succeed(resolution.transition);
|
|
340
|
+
};
|
|
341
|
+
const evaluateTransitions = (candidates, currentState, event) => {
|
|
342
|
+
const ctx = {
|
|
343
|
+
state: currentState,
|
|
344
|
+
event
|
|
345
|
+
};
|
|
346
|
+
const loop = (index, evaluations) => {
|
|
347
|
+
const candidate = candidates[index];
|
|
348
|
+
if (candidate === void 0) return {
|
|
349
|
+
transition: void 0,
|
|
350
|
+
evaluations
|
|
351
|
+
};
|
|
352
|
+
if (candidate.guard === void 0) return {
|
|
353
|
+
transition: candidate,
|
|
354
|
+
evaluations
|
|
355
|
+
};
|
|
356
|
+
const guardName = candidate.guard.name || "<inline>";
|
|
357
|
+
const result = candidate.guard(ctx);
|
|
358
|
+
const continueWith = (passed) => {
|
|
359
|
+
const nextEvaluations = [...evaluations, {
|
|
360
|
+
guard: guardName,
|
|
361
|
+
state: currentState,
|
|
362
|
+
event,
|
|
363
|
+
result: passed
|
|
364
|
+
}];
|
|
365
|
+
if (passed) return {
|
|
366
|
+
transition: candidate,
|
|
367
|
+
evaluations: nextEvaluations
|
|
368
|
+
};
|
|
369
|
+
return loop(index + 1, nextEvaluations);
|
|
370
|
+
};
|
|
371
|
+
if (!isEffect(result)) return continueWith(result);
|
|
372
|
+
return result.pipe(Effect.flatMap((passed) => {
|
|
373
|
+
const next = continueWith(passed);
|
|
374
|
+
if (isEffect(next)) return next;
|
|
375
|
+
return Effect.succeed(next);
|
|
376
|
+
}));
|
|
377
|
+
};
|
|
378
|
+
return loop(0, []);
|
|
201
379
|
};
|
|
202
380
|
//#endregion
|
|
203
|
-
export { executeTransition, executeTransitionImmediate, processEventCore, processEventCoreImmediate, resolveTransition, runSpawnEffects, shouldPostpone };
|
|
381
|
+
export { executeTransition, executeTransitionImmediate, processEventCore, processEventCoreImmediate, resolveTransition, resolveTransitionEffect, runSpawnEffects, shouldPostpone };
|
package/dist/internal/utils.js
CHANGED
|
@@ -64,6 +64,7 @@ const isEffect = Effect.isEffect;
|
|
|
64
64
|
const stubSystem = {
|
|
65
65
|
spawn: () => Effect.die("spawn not supported in stub system"),
|
|
66
66
|
get: () => Effect.die("get not supported in stub system"),
|
|
67
|
+
watch: () => Stream.die("watch not supported in stub system"),
|
|
67
68
|
stop: () => Effect.die("stop not supported in stub system"),
|
|
68
69
|
events: Stream.empty,
|
|
69
70
|
get actors() {
|