effect-machine 0.12.0 → 0.14.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 +133 -324
- package/dist/actor.d.ts +46 -28
- package/dist/actor.js +276 -315
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +3 -3
- package/dist/errors.d.ts +24 -20
- package/dist/errors.js +10 -6
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/internal/runtime.d.ts +82 -7
- package/dist/internal/runtime.js +162 -58
- package/dist/internal/transition.d.ts +12 -11
- package/dist/internal/transition.js +12 -14
- package/dist/machine.d.ts +148 -140
- package/dist/machine.js +141 -155
- package/dist/schema.d.ts +14 -0
- package/dist/schema.js +10 -1
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +21 -12
- package/dist/testing.js +23 -26
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +53 -30
- package/v3/dist/actor.js +286 -311
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +14 -10
- package/v3/dist/errors.js +11 -7
- package/v3/dist/index.d.ts +6 -5
- package/v3/dist/index.js +3 -2
- package/v3/dist/inspection.d.ts +3 -22
- package/v3/dist/inspection.js +1 -15
- package/v3/dist/internal/brands.d.ts +4 -8
- package/v3/dist/internal/inspection.js +1 -1
- package/v3/dist/internal/runtime.d.ts +87 -10
- package/v3/dist/internal/runtime.js +177 -61
- package/v3/dist/internal/transition.d.ts +13 -12
- package/v3/dist/internal/transition.js +14 -16
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +158 -143
- package/v3/dist/machine.js +148 -155
- package/v3/dist/schema.d.ts +25 -11
- package/v3/dist/schema.js +18 -5
- package/v3/dist/slot.d.ts +112 -86
- package/v3/dist/slot.js +92 -59
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +21 -12
- package/v3/dist/testing.js +23 -24
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { INTERNAL_INIT_EVENT } from "./utils.js";
|
|
2
2
|
import { NoReplyError } from "../errors.js";
|
|
3
3
|
import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
|
|
4
|
+
import { ActorExit } from "../supervision.js";
|
|
4
5
|
import { ActorSystem } from "../actor.js";
|
|
5
|
-
import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Runtime, Schema, Scope, SubscriptionRef } from "effect";
|
|
6
7
|
//#region src/internal/runtime.ts
|
|
7
8
|
/**
|
|
8
9
|
* Shared runtime kernel for machine event processing.
|
|
@@ -10,45 +11,65 @@ import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect
|
|
|
10
11
|
* Provides a single-queue event loop with:
|
|
11
12
|
* - Sequential event processing (no split-mailbox race)
|
|
12
13
|
* - Postpone buffer with drain-on-state-change (gen_statem)
|
|
13
|
-
* - Background effect lifecycle
|
|
14
|
+
* - Background effect lifecycle (under actorScope fault boundary)
|
|
14
15
|
* - Spawn effect lifecycle (per-state scope)
|
|
15
16
|
* - Final state detection → stop
|
|
16
17
|
* - Reply settlement (call/ask Deferreds)
|
|
17
18
|
* - Reply schema validation
|
|
19
|
+
* - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
|
|
20
|
+
* - ActorExit with exit reason (Final/Stopped/Defect) via exitDeferred
|
|
18
21
|
*
|
|
19
|
-
* Used by entity-machine
|
|
20
|
-
* with additional concerns (inspection, listeners, subscription ref, etc.)
|
|
21
|
-
* that will be migrated to use this kernel in a future refactor.
|
|
22
|
+
* Used by entity-machine and local actor (actor.ts delegates here).
|
|
22
23
|
*
|
|
23
24
|
* @internal
|
|
24
25
|
*/
|
|
25
26
|
/**
|
|
26
27
|
* Create a runtime for a machine. Returns a handle for sending events
|
|
27
28
|
* and querying state. The runtime owns:
|
|
28
|
-
* - Single event queue (all events serialized)
|
|
29
29
|
* - Event loop fiber
|
|
30
30
|
* - Postpone buffer
|
|
31
|
-
* - Background effects
|
|
31
|
+
* - Background effects (under actorScope)
|
|
32
32
|
* - State scope (spawn effects)
|
|
33
33
|
* - Final state detection
|
|
34
|
+
* - Exit reason via exitDeferred
|
|
35
|
+
*
|
|
36
|
+
* Resources (stateRef, eventQueue, stoppedRef) are either cell-provided
|
|
37
|
+
* or allocated fresh by the runtime.
|
|
34
38
|
*
|
|
35
39
|
* @internal
|
|
36
40
|
*/
|
|
37
41
|
const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
|
|
38
|
-
const { actorId, hooks } = config;
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
+
const { actorId, hooks, lifecycle } = config;
|
|
43
|
+
const rt = yield* Effect.runtime();
|
|
44
|
+
const fork = Runtime.runFork(rt);
|
|
45
|
+
const stateRef = config.cellResources?.stateRef ?? (yield* SubscriptionRef.make(machine.initial));
|
|
46
|
+
const stoppedRef = config.cellResources?.stoppedRef ?? (yield* Ref.make(false));
|
|
47
|
+
const eventQueue = config.cellResources?.eventQueue ?? (yield* config.queueFactory ?? Queue.unbounded());
|
|
48
|
+
const exitDeferred = yield* Deferred.make();
|
|
49
|
+
const actorScope = yield* Scope.make();
|
|
50
|
+
const deferredReplyRef = { current: void 0 };
|
|
42
51
|
const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
|
|
43
52
|
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
44
53
|
_tag: "send",
|
|
45
54
|
event
|
|
46
55
|
});
|
|
47
56
|
});
|
|
57
|
+
const childPrefix = config.childIdPrefix ?? "";
|
|
58
|
+
const defaultSpawn = (childId, childMachine) => system.spawn(`${childPrefix}${childId}`, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
59
|
+
const onChildSpawned = config.onChildSpawned;
|
|
48
60
|
const self = {
|
|
49
61
|
send: selfSend,
|
|
50
62
|
cast: selfSend,
|
|
51
|
-
spawn: (childId, childMachine) =>
|
|
63
|
+
spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn,
|
|
64
|
+
reply: (value) => Effect.sync(() => {
|
|
65
|
+
const deferred = deferredReplyRef.current;
|
|
66
|
+
if (deferred !== void 0) {
|
|
67
|
+
deferredReplyRef.current = void 0;
|
|
68
|
+
fork(Deferred.succeed(deferred, value));
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
})
|
|
52
73
|
};
|
|
53
74
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
54
75
|
const backgroundFibers = [];
|
|
@@ -60,36 +81,67 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
60
81
|
self,
|
|
61
82
|
system
|
|
62
83
|
};
|
|
63
|
-
const
|
|
84
|
+
const slots = machine._slots;
|
|
64
85
|
for (const bg of machine.backgroundEffects) {
|
|
65
|
-
const fiber = yield*
|
|
86
|
+
const fiber = yield* bg.handler({
|
|
66
87
|
actorId,
|
|
67
88
|
state: machine.initial,
|
|
68
89
|
event: initEvent,
|
|
69
90
|
self,
|
|
70
|
-
|
|
91
|
+
slots,
|
|
71
92
|
system
|
|
72
|
-
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
93
|
+
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
73
94
|
backgroundFibers.push(fiber);
|
|
74
95
|
}
|
|
75
|
-
yield*
|
|
96
|
+
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
|
|
97
|
+
const loopFiberRef = { current: void 0 };
|
|
98
|
+
const initialSpawnDefectSignal = (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid);
|
|
99
|
+
yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
|
|
100
|
+
return Effect.gen(function* () {
|
|
101
|
+
yield* Ref.set(stoppedRef, true);
|
|
102
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
103
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
104
|
+
yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
|
|
105
|
+
return yield* Effect.failCause(cause);
|
|
106
|
+
});
|
|
107
|
+
}));
|
|
108
|
+
/** Set the exit deferred exactly once. */
|
|
109
|
+
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
76
110
|
if (machine.finalStates.has(machine.initial._tag)) {
|
|
111
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
|
|
77
112
|
yield* Ref.set(stoppedRef, true);
|
|
78
113
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
79
|
-
yield*
|
|
80
|
-
|
|
114
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
115
|
+
yield* setExit(ActorExit.Final(machine.initial));
|
|
116
|
+
return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
|
|
81
117
|
}
|
|
82
|
-
const
|
|
118
|
+
const augmentedHooks = {
|
|
119
|
+
...hooks,
|
|
120
|
+
onSpawnDefect: (cause) => Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "spawn")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Effect.suspend(() => loopFiberRef.current !== void 0 ? Fiber.interrupt(loopFiberRef.current) : Effect.void)), Effect.asVoid)
|
|
121
|
+
};
|
|
122
|
+
const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
|
|
123
|
+
loopFiberRef.current = loopFiber;
|
|
124
|
+
if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
|
|
125
|
+
if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
|
|
126
|
+
return Effect.never;
|
|
127
|
+
})))).pipe(Effect.forkIn(actorScope));
|
|
128
|
+
yield* Effect.forkDaemon(Effect.gen(function* () {
|
|
129
|
+
const loopExit = yield* Fiber.await(loopFiber);
|
|
130
|
+
if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
|
|
131
|
+
else yield* Scope.close(actorScope, loopExit);
|
|
132
|
+
}));
|
|
83
133
|
const stop = Effect.gen(function* () {
|
|
84
134
|
if (yield* Ref.get(stoppedRef)) return;
|
|
135
|
+
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
85
136
|
yield* Ref.set(stoppedRef, true);
|
|
86
137
|
yield* Fiber.interrupt(loopFiber);
|
|
87
138
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
88
|
-
yield*
|
|
139
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
140
|
+
yield* setExit(ActorExit.Stopped);
|
|
89
141
|
}).pipe(Effect.asVoid);
|
|
90
|
-
yield* Effect.addFinalizer(() => stop);
|
|
142
|
+
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
91
143
|
return {
|
|
92
|
-
...makeHandle(stateRef, stoppedRef, eventQueue,
|
|
144
|
+
...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
|
|
93
145
|
stop
|
|
94
146
|
};
|
|
95
147
|
});
|
|
@@ -97,7 +149,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
97
149
|
* Build the runtime handle (send/ask/getState/isStopped).
|
|
98
150
|
* Shared between initial-final and normal paths.
|
|
99
151
|
*/
|
|
100
|
-
const makeHandle = (stateRef, stoppedRef, eventQueue,
|
|
152
|
+
const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
|
|
101
153
|
send: (event) => Effect.gen(function* () {
|
|
102
154
|
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
103
155
|
_tag: "send",
|
|
@@ -128,30 +180,68 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
|
|
|
128
180
|
});
|
|
129
181
|
return yield* Deferred.await(reply);
|
|
130
182
|
}),
|
|
131
|
-
getState:
|
|
183
|
+
getState: SubscriptionRef.get(stateRef),
|
|
184
|
+
stateRef,
|
|
132
185
|
isStopped: Ref.get(stoppedRef),
|
|
133
|
-
stop: Effect.void
|
|
186
|
+
stop: Effect.void,
|
|
187
|
+
_queue: eventQueue,
|
|
188
|
+
_stoppedRef: stoppedRef,
|
|
189
|
+
exitDeferred,
|
|
190
|
+
actorScope
|
|
134
191
|
});
|
|
135
|
-
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self,
|
|
192
|
+
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, wrapProcess, fork) {
|
|
193
|
+
const forkEffect = fork ?? Effect.runFork;
|
|
194
|
+
/** Set the exit deferred exactly once. */
|
|
195
|
+
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
136
196
|
const postponed = [];
|
|
137
197
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
138
198
|
const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
|
|
139
199
|
const event = queued.event;
|
|
140
|
-
const currentState = yield*
|
|
200
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
141
201
|
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
202
|
+
if (queued._tag === "call") {
|
|
203
|
+
const postponedResult = {
|
|
204
|
+
newState: currentState,
|
|
205
|
+
previousState: currentState,
|
|
206
|
+
transitioned: false,
|
|
207
|
+
lifecycleRan: false,
|
|
208
|
+
isFinal: false,
|
|
209
|
+
hasReply: false,
|
|
210
|
+
deferReply: false,
|
|
211
|
+
reply: void 0,
|
|
212
|
+
postponed: true
|
|
213
|
+
};
|
|
214
|
+
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
215
|
+
}
|
|
216
|
+
if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
|
|
142
217
|
postponed.push({
|
|
143
218
|
_tag: "send",
|
|
144
219
|
event
|
|
145
220
|
});
|
|
146
|
-
if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
|
|
147
221
|
return {
|
|
148
222
|
shouldStop: false,
|
|
149
|
-
stateChanged: false
|
|
223
|
+
stateChanged: false,
|
|
224
|
+
result: {
|
|
225
|
+
newState: currentState,
|
|
226
|
+
previousState: currentState,
|
|
227
|
+
transitioned: false,
|
|
228
|
+
lifecycleRan: false,
|
|
229
|
+
isFinal: false,
|
|
230
|
+
hasReply: false,
|
|
231
|
+
deferReply: false,
|
|
232
|
+
reply: void 0,
|
|
233
|
+
postponed: true
|
|
234
|
+
}
|
|
150
235
|
};
|
|
151
236
|
}
|
|
237
|
+
if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
|
|
152
238
|
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
153
|
-
if (result.transitioned) yield*
|
|
239
|
+
if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
|
|
240
|
+
if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
|
|
154
241
|
switch (queued._tag) {
|
|
242
|
+
case "call":
|
|
243
|
+
yield* Deferred.succeed(queued.reply, result);
|
|
244
|
+
break;
|
|
155
245
|
case "sendWait":
|
|
156
246
|
yield* Deferred.succeed(queued.done, void 0);
|
|
157
247
|
break;
|
|
@@ -168,35 +258,69 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
168
258
|
}
|
|
169
259
|
yield* Deferred.succeed(queued.reply, decoded);
|
|
170
260
|
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
171
|
-
} else
|
|
261
|
+
} else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
|
|
262
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
172
263
|
actorId,
|
|
173
264
|
eventTag: event._tag
|
|
174
265
|
}));
|
|
175
266
|
break;
|
|
176
267
|
}
|
|
268
|
+
if (lifecycle?.onProcessed !== void 0 && result.transitioned) yield* lifecycle.onProcessed(result, event);
|
|
269
|
+
const shouldStop = result.isFinal && result.lifecycleRan;
|
|
270
|
+
if (shouldStop && lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(result.newState);
|
|
177
271
|
return {
|
|
178
|
-
shouldStop
|
|
179
|
-
stateChanged: result.lifecycleRan
|
|
272
|
+
shouldStop,
|
|
273
|
+
stateChanged: result.lifecycleRan,
|
|
274
|
+
result
|
|
180
275
|
};
|
|
181
276
|
});
|
|
277
|
+
const shutdown = (exitReason) => Effect.gen(function* () {
|
|
278
|
+
yield* Ref.set(stoppedRef, true);
|
|
279
|
+
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
280
|
+
settlePostponed(postponed, actorId, forkEffect);
|
|
281
|
+
const remaining = yield* Queue.takeAll(eventQueue);
|
|
282
|
+
for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
|
|
283
|
+
else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
|
|
284
|
+
actorId,
|
|
285
|
+
eventTag: entry.event._tag
|
|
286
|
+
})));
|
|
287
|
+
else if (entry._tag === "call") {
|
|
288
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
289
|
+
forkEffect(Deferred.succeed(entry.reply, {
|
|
290
|
+
newState: currentState,
|
|
291
|
+
previousState: currentState,
|
|
292
|
+
transitioned: false,
|
|
293
|
+
lifecycleRan: false,
|
|
294
|
+
isFinal: machine.finalStates.has(currentState._tag),
|
|
295
|
+
hasReply: false,
|
|
296
|
+
deferReply: false,
|
|
297
|
+
reply: void 0,
|
|
298
|
+
postponed: false
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
302
|
+
yield* setExit(exitReason);
|
|
303
|
+
});
|
|
182
304
|
while (true) {
|
|
183
305
|
const queued = yield* Queue.take(eventQueue);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
306
|
+
if (queued._tag === "drain") {
|
|
307
|
+
yield* shutdown(ActorExit.Stopped);
|
|
308
|
+
yield* Deferred.succeed(queued.done, void 0);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const eventQueued = queued;
|
|
312
|
+
const processInner = processQueued(eventQueued);
|
|
313
|
+
const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
|
|
314
|
+
return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
|
|
315
|
+
}) : processInner).pipe(Effect.catchAllCause((cause) => {
|
|
316
|
+
if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
|
|
317
|
+
else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
|
|
318
|
+
else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
|
|
319
|
+
return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
188
320
|
}));
|
|
189
321
|
if (shouldStop) {
|
|
190
|
-
yield*
|
|
191
|
-
|
|
192
|
-
const remaining = yield* Queue.takeAll(eventQueue);
|
|
193
|
-
for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
|
|
194
|
-
else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
|
|
195
|
-
actorId,
|
|
196
|
-
eventTag: entry.event._tag
|
|
197
|
-
})));
|
|
198
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
199
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
322
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
323
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
200
324
|
return;
|
|
201
325
|
}
|
|
202
326
|
let drainTriggered = stateChanged;
|
|
@@ -206,16 +330,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
206
330
|
for (const entry of drained) {
|
|
207
331
|
const drain = yield* processQueued(entry);
|
|
208
332
|
if (drain.shouldStop) {
|
|
209
|
-
yield*
|
|
210
|
-
|
|
211
|
-
const remaining2 = yield* Queue.takeAll(eventQueue);
|
|
212
|
-
for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
|
|
213
|
-
else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
|
|
214
|
-
actorId,
|
|
215
|
-
eventTag: r.event._tag
|
|
216
|
-
})));
|
|
217
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
218
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
333
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
334
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
219
335
|
return;
|
|
220
336
|
}
|
|
221
337
|
if (drain.stateChanged) drainTriggered = true;
|
|
@@ -224,12 +340,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
224
340
|
}
|
|
225
341
|
});
|
|
226
342
|
/** Settle all pending Deferreds in the postpone buffer on shutdown. */
|
|
227
|
-
const settlePostponed = (postponed, actorId) => {
|
|
228
|
-
for (const entry of postponed) if (entry._tag === "ask")
|
|
343
|
+
const settlePostponed = (postponed, actorId, forkFn) => {
|
|
344
|
+
for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
|
|
229
345
|
actorId,
|
|
230
346
|
eventTag: entry.event._tag
|
|
231
347
|
})));
|
|
232
|
-
else if (entry._tag === "sendWait")
|
|
348
|
+
else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
|
|
233
349
|
postponed.length = 0;
|
|
234
350
|
};
|
|
235
351
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { MachineContext, SlotsDef } from "../slot.js";
|
|
2
|
+
import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
|
|
3
3
|
import { ActorSystem } from "../actor.js";
|
|
4
4
|
import { Cause, Effect, Scope } from "effect";
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ interface TransitionExecutionResult<S> {
|
|
|
21
21
|
*
|
|
22
22
|
* Used by:
|
|
23
23
|
* - executeTransition (actor event loop, testing)
|
|
24
|
-
* -
|
|
24
|
+
* - Machine.replay (event sourcing restore)
|
|
25
25
|
*
|
|
26
26
|
* @internal
|
|
27
27
|
*/
|
|
@@ -29,7 +29,7 @@ declare const runTransitionHandler: <S extends {
|
|
|
29
29
|
readonly _tag: string;
|
|
30
30
|
}, E extends {
|
|
31
31
|
readonly _tag: string;
|
|
32
|
-
}, R,
|
|
32
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, transition: Transition<S, E, SD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
|
|
33
33
|
newState: S;
|
|
34
34
|
hasReply: boolean;
|
|
35
35
|
deferReply: boolean;
|
|
@@ -50,7 +50,7 @@ declare const executeTransition: <S extends {
|
|
|
50
50
|
readonly _tag: string;
|
|
51
51
|
}, E extends {
|
|
52
52
|
readonly _tag: string;
|
|
53
|
-
}, R,
|
|
53
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
|
|
54
54
|
newState: S;
|
|
55
55
|
transitioned: boolean;
|
|
56
56
|
reenter: boolean;
|
|
@@ -68,6 +68,8 @@ interface ProcessEventHooks<S, E> {
|
|
|
68
68
|
readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
|
|
69
69
|
/** Called when a transition handler or spawn effect fails with a defect */
|
|
70
70
|
readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
|
|
71
|
+
/** Called when a forked spawn fiber defects — signals the runtime to set exitDeferred */
|
|
72
|
+
readonly onSpawnDefect?: (cause: Cause.Cause<unknown>) => Effect.Effect<void>;
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
75
|
* Error info for inspection hooks.
|
|
@@ -109,7 +111,7 @@ declare const shouldPostpone: <S extends {
|
|
|
109
111
|
readonly _tag: string;
|
|
110
112
|
}, E extends {
|
|
111
113
|
readonly _tag: string;
|
|
112
|
-
}, R>(machine: Machine<S, E, R, any, any, any
|
|
114
|
+
}, R>(machine: Machine<S, E, R, any, any, any>, stateTag: string, eventTag: string) => boolean;
|
|
113
115
|
/**
|
|
114
116
|
* Process a single event through the machine.
|
|
115
117
|
*
|
|
@@ -126,7 +128,7 @@ declare const processEventCore: <S extends {
|
|
|
126
128
|
readonly _tag: string;
|
|
127
129
|
}, E extends {
|
|
128
130
|
readonly _tag: string;
|
|
129
|
-
}, R,
|
|
131
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
|
|
130
132
|
current: Scope.CloseableScope;
|
|
131
133
|
}, system: ActorSystem, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
|
|
132
134
|
newState: S;
|
|
@@ -148,7 +150,7 @@ declare const runSpawnEffects: <S extends {
|
|
|
148
150
|
readonly _tag: string;
|
|
149
151
|
}, E extends {
|
|
150
152
|
readonly _tag: string;
|
|
151
|
-
}, R,
|
|
153
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined, onSpawnDefect?: ((cause: Cause.Cause<unknown>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
152
154
|
/**
|
|
153
155
|
* Resolve which transition should fire for a given state and event.
|
|
154
156
|
* Uses indexed O(1) lookup. First matching transition wins.
|
|
@@ -157,7 +159,7 @@ declare const resolveTransition: <S extends {
|
|
|
157
159
|
readonly _tag: string;
|
|
158
160
|
}, E extends {
|
|
159
161
|
readonly _tag: string;
|
|
160
|
-
}, R>(machine: Machine<S, E, R, any, any, any
|
|
162
|
+
}, R>(machine: Machine<S, E, R, any, any, any>, currentState: S, event: E) => (typeof machine.transitions)[number] | undefined;
|
|
161
163
|
/**
|
|
162
164
|
* Invalidate cached index for a machine (call after mutation).
|
|
163
165
|
*/
|
|
@@ -166,14 +168,13 @@ declare const invalidateIndex: (machine: object) => void;
|
|
|
166
168
|
* Find all transitions matching a state/event pair.
|
|
167
169
|
* Returns empty array if no matches.
|
|
168
170
|
*
|
|
169
|
-
* Accepts both `Machine` and `BuiltMachine`.
|
|
170
171
|
* O(1) lookup after first access (index is lazily built).
|
|
171
172
|
*/
|
|
172
173
|
declare const findTransitions: <S extends {
|
|
173
174
|
readonly _tag: string;
|
|
174
175
|
}, E extends {
|
|
175
176
|
readonly _tag: string;
|
|
176
|
-
}, R,
|
|
177
|
+
}, R, SD extends SlotsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, SD>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, SD, R>>;
|
|
177
178
|
/**
|
|
178
179
|
* Find all spawn effects for a state.
|
|
179
180
|
* Returns empty array if no matches.
|
|
@@ -184,6 +185,6 @@ declare const findSpawnEffects: <S extends {
|
|
|
184
185
|
readonly _tag: string;
|
|
185
186
|
}, E extends {
|
|
186
187
|
readonly _tag: string;
|
|
187
|
-
}, R,
|
|
188
|
+
}, R, SD extends SlotsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, SD>, stateTag: string) => ReadonlyArray<SpawnEffect<S, E, SD, R>>;
|
|
188
189
|
//#endregion
|
|
189
190
|
export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
|
|
2
|
-
import { BuiltMachine } from "../machine.js";
|
|
3
2
|
import { Cause, Effect, Exit, Scope } from "effect";
|
|
4
3
|
//#region src/internal/transition.ts
|
|
5
4
|
/**
|
|
@@ -18,7 +17,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
|
|
|
18
17
|
*
|
|
19
18
|
* Used by:
|
|
20
19
|
* - executeTransition (actor event loop, testing)
|
|
21
|
-
* -
|
|
20
|
+
* - Machine.replay (event sourcing restore)
|
|
22
21
|
*
|
|
23
22
|
* @internal
|
|
24
23
|
*/
|
|
@@ -30,12 +29,10 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
|
|
|
30
29
|
self,
|
|
31
30
|
system
|
|
32
31
|
};
|
|
33
|
-
const { guards, effects } = machine._slots;
|
|
34
32
|
const handlerCtx = {
|
|
35
33
|
state,
|
|
36
34
|
event,
|
|
37
|
-
|
|
38
|
-
effects
|
|
35
|
+
slots: machine._slots
|
|
39
36
|
};
|
|
40
37
|
const raw = transition.handler(handlerCtx);
|
|
41
38
|
const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
|
|
@@ -119,7 +116,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
119
116
|
state: currentState,
|
|
120
117
|
event,
|
|
121
118
|
cause
|
|
122
|
-
}).pipe(Effect.
|
|
119
|
+
}).pipe(Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
123
120
|
}));
|
|
124
121
|
if (!result.transitioned) return {
|
|
125
122
|
newState: currentState,
|
|
@@ -139,7 +136,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
139
136
|
stateScopeRef.current = yield* Scope.make();
|
|
140
137
|
if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
|
|
141
138
|
if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
|
|
142
|
-
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError);
|
|
139
|
+
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
|
|
143
140
|
}
|
|
144
141
|
return {
|
|
145
142
|
newState,
|
|
@@ -158,7 +155,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
158
155
|
*
|
|
159
156
|
* @internal
|
|
160
157
|
*/
|
|
161
|
-
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError) {
|
|
158
|
+
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
|
|
162
159
|
const spawnEffects = findSpawnEffects(machine, state._tag);
|
|
163
160
|
const ctx = {
|
|
164
161
|
actorId,
|
|
@@ -167,25 +164,27 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
167
164
|
self,
|
|
168
165
|
system
|
|
169
166
|
};
|
|
170
|
-
const
|
|
167
|
+
const slots = machine._slots;
|
|
171
168
|
const reportError = onError;
|
|
169
|
+
const defectSignal = onSpawnDefect;
|
|
172
170
|
for (const spawnEffect of spawnEffects) {
|
|
173
171
|
const effect = spawnEffect.handler({
|
|
174
172
|
actorId,
|
|
175
173
|
state,
|
|
176
174
|
event,
|
|
177
175
|
self,
|
|
178
|
-
|
|
176
|
+
slots,
|
|
179
177
|
system
|
|
180
178
|
}).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
|
|
181
179
|
if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
|
|
182
|
-
|
|
183
|
-
return reportError({
|
|
180
|
+
const report = reportError !== void 0 ? reportError({
|
|
184
181
|
phase: "spawn",
|
|
185
182
|
state,
|
|
186
183
|
event,
|
|
187
184
|
cause
|
|
188
|
-
})
|
|
185
|
+
}) : Effect.void;
|
|
186
|
+
const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
|
|
187
|
+
return report.pipe(Effect.andThen(signal), Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
189
188
|
}));
|
|
190
189
|
yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
|
|
191
190
|
}
|
|
@@ -258,11 +257,10 @@ const getIndex = (machine) => {
|
|
|
258
257
|
* Find all transitions matching a state/event pair.
|
|
259
258
|
* Returns empty array if no matches.
|
|
260
259
|
*
|
|
261
|
-
* Accepts both `Machine` and `BuiltMachine`.
|
|
262
260
|
* O(1) lookup after first access (index is lazily built).
|
|
263
261
|
*/
|
|
264
|
-
const findTransitions = (
|
|
265
|
-
const index = getIndex(
|
|
262
|
+
const findTransitions = (machine, stateTag, eventTag) => {
|
|
263
|
+
const index = getIndex(machine);
|
|
266
264
|
const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
|
|
267
265
|
if (specific.length > 0) return specific;
|
|
268
266
|
return index.transitions.get("*")?.get(eventTag) ?? [];
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
2
|
//#region src/internal/utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Internal utilities for effect-machine.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
3
7
|
const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
|
|
4
8
|
/**
|
|
5
9
|
* Create a reply result for ask-bearing event handlers.
|
|
@@ -50,7 +54,7 @@ const getTag = (constructorOrValue) => {
|
|
|
50
54
|
}
|
|
51
55
|
};
|
|
52
56
|
/** Check if a value is an Effect */
|
|
53
|
-
const isEffect =
|
|
57
|
+
const isEffect = Effect.isEffect;
|
|
54
58
|
/**
|
|
55
59
|
* Stub ActorSystem that dies on any method call.
|
|
56
60
|
* Used in contexts where spawning/system access isn't supported
|