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
package/dist/internal/runtime.js
CHANGED
|
@@ -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, SubscriptionRef } from "effect";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
|
|
6
7
|
//#region src/internal/runtime.ts
|
|
7
8
|
/**
|
|
8
9
|
* Shared runtime kernel for machine event processing.
|
|
@@ -10,35 +11,42 @@ import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionR
|
|
|
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 services = yield* Effect.services();
|
|
44
|
+
const fork = Effect.runForkWith(services);
|
|
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();
|
|
42
50
|
const deferredReplyRef = { current: void 0 };
|
|
43
51
|
const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
|
|
44
52
|
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
@@ -46,15 +54,18 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
46
54
|
event
|
|
47
55
|
});
|
|
48
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;
|
|
49
60
|
const self = {
|
|
50
61
|
send: selfSend,
|
|
51
62
|
cast: selfSend,
|
|
52
|
-
spawn: (childId, childMachine) =>
|
|
63
|
+
spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn,
|
|
53
64
|
reply: (value) => Effect.sync(() => {
|
|
54
65
|
const deferred = deferredReplyRef.current;
|
|
55
66
|
if (deferred !== void 0) {
|
|
56
67
|
deferredReplyRef.current = void 0;
|
|
57
|
-
|
|
68
|
+
fork(Deferred.succeed(deferred, value));
|
|
58
69
|
return true;
|
|
59
70
|
}
|
|
60
71
|
return false;
|
|
@@ -70,36 +81,67 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
70
81
|
self,
|
|
71
82
|
system
|
|
72
83
|
};
|
|
73
|
-
const
|
|
84
|
+
const slots = machine._slots;
|
|
74
85
|
for (const bg of machine.backgroundEffects) {
|
|
75
|
-
const fiber = yield*
|
|
86
|
+
const fiber = yield* bg.handler({
|
|
76
87
|
actorId,
|
|
77
88
|
state: machine.initial,
|
|
78
89
|
event: initEvent,
|
|
79
90
|
self,
|
|
80
|
-
|
|
91
|
+
slots,
|
|
81
92
|
system
|
|
82
|
-
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
93
|
+
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
83
94
|
backgroundFibers.push(fiber);
|
|
84
95
|
}
|
|
85
|
-
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.catchCause((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);
|
|
86
110
|
if (machine.finalStates.has(machine.initial._tag)) {
|
|
111
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
|
|
87
112
|
yield* Ref.set(stoppedRef, true);
|
|
88
113
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
89
|
-
yield*
|
|
90
|
-
|
|
114
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
115
|
+
yield* setExit(ActorExit.Final(machine.initial));
|
|
116
|
+
return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
|
|
91
117
|
}
|
|
92
|
-
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.forkDetach(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.hasInterruptsOnly(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.forkDetach(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
|
+
}));
|
|
93
133
|
const stop = Effect.gen(function* () {
|
|
94
134
|
if (yield* Ref.get(stoppedRef)) return;
|
|
135
|
+
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
95
136
|
yield* Ref.set(stoppedRef, true);
|
|
96
137
|
yield* Fiber.interrupt(loopFiber);
|
|
97
138
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
98
|
-
yield*
|
|
139
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
140
|
+
yield* setExit(ActorExit.Stopped);
|
|
99
141
|
}).pipe(Effect.asVoid);
|
|
100
|
-
yield* Effect.addFinalizer(() => stop);
|
|
142
|
+
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
101
143
|
return {
|
|
102
|
-
...makeHandle(stateRef, stoppedRef, eventQueue,
|
|
144
|
+
...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
|
|
103
145
|
stop
|
|
104
146
|
};
|
|
105
147
|
});
|
|
@@ -107,7 +149,7 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
107
149
|
* Build the runtime handle (send/ask/getState/isStopped).
|
|
108
150
|
* Shared between initial-final and normal paths.
|
|
109
151
|
*/
|
|
110
|
-
const makeHandle = (stateRef, stoppedRef, eventQueue,
|
|
152
|
+
const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
|
|
111
153
|
send: (event) => Effect.gen(function* () {
|
|
112
154
|
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
113
155
|
_tag: "send",
|
|
@@ -141,28 +183,65 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
|
|
|
141
183
|
getState: SubscriptionRef.get(stateRef),
|
|
142
184
|
stateRef,
|
|
143
185
|
isStopped: Ref.get(stoppedRef),
|
|
144
|
-
stop: Effect.void
|
|
186
|
+
stop: Effect.void,
|
|
187
|
+
_queue: eventQueue,
|
|
188
|
+
_stoppedRef: stoppedRef,
|
|
189
|
+
exitDeferred,
|
|
190
|
+
actorScope
|
|
145
191
|
});
|
|
146
|
-
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);
|
|
147
196
|
const postponed = [];
|
|
148
197
|
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
149
198
|
const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
|
|
150
199
|
const event = queued.event;
|
|
151
200
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
152
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);
|
|
153
217
|
postponed.push({
|
|
154
218
|
_tag: "send",
|
|
155
219
|
event
|
|
156
220
|
});
|
|
157
|
-
if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
|
|
158
221
|
return {
|
|
159
222
|
shouldStop: false,
|
|
160
|
-
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
|
+
}
|
|
161
235
|
};
|
|
162
236
|
}
|
|
237
|
+
if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
|
|
163
238
|
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
164
239
|
if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
|
|
240
|
+
if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
|
|
165
241
|
switch (queued._tag) {
|
|
242
|
+
case "call":
|
|
243
|
+
yield* Deferred.succeed(queued.reply, result);
|
|
244
|
+
break;
|
|
166
245
|
case "sendWait":
|
|
167
246
|
yield* Deferred.succeed(queued.done, void 0);
|
|
168
247
|
break;
|
|
@@ -186,29 +265,62 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
186
265
|
}));
|
|
187
266
|
break;
|
|
188
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);
|
|
189
271
|
return {
|
|
190
|
-
shouldStop
|
|
191
|
-
stateChanged: result.lifecycleRan
|
|
272
|
+
shouldStop,
|
|
273
|
+
stateChanged: result.lifecycleRan,
|
|
274
|
+
result
|
|
192
275
|
};
|
|
193
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.clear(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
|
+
});
|
|
194
304
|
while (true) {
|
|
195
305
|
const queued = yield* Queue.take(eventQueue);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
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.catchCause((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)));
|
|
200
320
|
}));
|
|
201
321
|
if (shouldStop) {
|
|
202
|
-
yield*
|
|
203
|
-
|
|
204
|
-
const remaining = yield* Queue.takeAll(eventQueue);
|
|
205
|
-
for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
|
|
206
|
-
else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
|
|
207
|
-
actorId,
|
|
208
|
-
eventTag: entry.event._tag
|
|
209
|
-
})));
|
|
210
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
211
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
322
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
323
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
212
324
|
return;
|
|
213
325
|
}
|
|
214
326
|
let drainTriggered = stateChanged;
|
|
@@ -218,16 +330,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
218
330
|
for (const entry of drained) {
|
|
219
331
|
const drain = yield* processQueued(entry);
|
|
220
332
|
if (drain.shouldStop) {
|
|
221
|
-
yield*
|
|
222
|
-
|
|
223
|
-
const remaining2 = yield* Queue.takeAll(eventQueue);
|
|
224
|
-
for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
|
|
225
|
-
else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
|
|
226
|
-
actorId,
|
|
227
|
-
eventTag: r.event._tag
|
|
228
|
-
})));
|
|
229
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
230
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
333
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
334
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
231
335
|
return;
|
|
232
336
|
}
|
|
233
337
|
if (drain.stateChanged) drainTriggered = true;
|
|
@@ -236,12 +340,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
236
340
|
}
|
|
237
341
|
});
|
|
238
342
|
/** Settle all pending Deferreds in the postpone buffer on shutdown. */
|
|
239
|
-
const settlePostponed = (postponed, actorId) => {
|
|
240
|
-
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({
|
|
241
345
|
actorId,
|
|
242
346
|
eventTag: entry.event._tag
|
|
243
347
|
})));
|
|
244
|
-
else if (entry._tag === "sendWait")
|
|
348
|
+
else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
|
|
245
349
|
postponed.length = 0;
|
|
246
350
|
};
|
|
247
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
|
|
|
@@ -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.Closeable;
|
|
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.Closeable, 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
|
/**
|
|
@@ -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;
|
|
@@ -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.catchCause((cause) => {
|
|
181
179
|
if (Cause.hasInterruptsOnly(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) ?? [];
|