effect-machine 0.11.0 → 0.13.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 +128 -324
- package/dist/actor.d.ts +52 -31
- package/dist/actor.js +218 -283
- package/dist/cluster/adapters/in-memory.d.ts +28 -0
- package/dist/cluster/adapters/in-memory.js +79 -0
- package/dist/cluster/entity-actor-ref.d.ts +56 -0
- package/dist/cluster/entity-actor-ref.js +33 -0
- package/dist/cluster/entity-machine.d.ts +31 -49
- package/dist/cluster/entity-machine.js +178 -52
- package/dist/cluster/index.d.ts +5 -2
- package/dist/cluster/index.js +4 -1
- package/dist/cluster/persistence.d.ts +49 -0
- package/dist/cluster/persistence.js +18 -0
- package/dist/cluster/to-entity.d.ts +9 -3
- package/dist/cluster/to-entity.js +16 -4
- package/dist/errors.d.ts +25 -17
- package/dist/errors.js +10 -5
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +142 -0
- package/dist/internal/runtime.js +357 -0
- package/dist/internal/transition.d.ts +10 -4
- package/dist/internal/transition.js +24 -12
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +89 -55
- package/dist/machine.js +80 -68
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +33 -4
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +17 -8
- package/dist/testing.js +22 -23
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +54 -37
- package/v3/dist/actor.js +209 -277
- package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
- package/v3/dist/cluster/adapters/in-memory.js +62 -0
- package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
- package/v3/dist/cluster/entity-actor-ref.js +19 -0
- package/v3/dist/cluster/entity-machine.d.ts +34 -49
- package/v3/dist/cluster/entity-machine.js +134 -50
- package/v3/dist/cluster/index.d.ts +5 -2
- package/v3/dist/cluster/index.js +4 -1
- package/v3/dist/cluster/persistence.d.ts +48 -0
- package/v3/dist/cluster/persistence.js +14 -0
- package/v3/dist/cluster/to-entity.d.ts +5 -2
- package/v3/dist/cluster/to-entity.js +12 -4
- package/v3/dist/errors.d.ts +18 -8
- package/v3/dist/errors.js +9 -4
- package/v3/dist/index.d.ts +6 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +142 -0
- package/v3/dist/internal/runtime.js +335 -0
- package/v3/dist/internal/transition.d.ts +10 -4
- package/v3/dist/internal/transition.js +23 -11
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +35 -47
- package/v3/dist/machine.js +62 -64
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +18 -9
- package/v3/dist/testing.js +21 -22
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { INTERNAL_INIT_EVENT } from "./utils.js";
|
|
2
|
+
import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
|
|
3
|
+
import { NoReplyError } from "../errors.js";
|
|
4
|
+
import { ActorExit } from "../supervision.js";
|
|
5
|
+
import { ActorSystem } from "../actor.js";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Option, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
|
|
7
|
+
//#region src/internal/runtime.ts
|
|
8
|
+
/**
|
|
9
|
+
* Shared runtime kernel for machine event processing.
|
|
10
|
+
*
|
|
11
|
+
* Provides a single-queue event loop with:
|
|
12
|
+
* - Sequential event processing (no split-mailbox race)
|
|
13
|
+
* - Postpone buffer with drain-on-state-change (gen_statem)
|
|
14
|
+
* - Background effect lifecycle (under actorScope fault boundary)
|
|
15
|
+
* - Spawn effect lifecycle (per-state scope)
|
|
16
|
+
* - Final state detection → stop
|
|
17
|
+
* - Reply settlement (call/ask Deferreds)
|
|
18
|
+
* - Reply schema validation
|
|
19
|
+
* - Lifecycle hooks for actor-specific concerns (inspection, listeners, etc.)
|
|
20
|
+
* - ActorExit with exit reason (Final/Stopped/Defect) via exitDeferred
|
|
21
|
+
*
|
|
22
|
+
* Used by entity-machine and local actor (actor.ts delegates here).
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Create a runtime for a machine. Returns a handle for sending events
|
|
28
|
+
* and querying state. The runtime owns:
|
|
29
|
+
* - Event loop fiber
|
|
30
|
+
* - Postpone buffer
|
|
31
|
+
* - Background effects (under actorScope)
|
|
32
|
+
* - State scope (spawn effects)
|
|
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.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
|
|
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();
|
|
50
|
+
const deferredReplyRef = { current: void 0 };
|
|
51
|
+
const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
|
|
52
|
+
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
53
|
+
_tag: "send",
|
|
54
|
+
event
|
|
55
|
+
});
|
|
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;
|
|
60
|
+
const self = {
|
|
61
|
+
send: selfSend,
|
|
62
|
+
cast: selfSend,
|
|
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
|
+
})
|
|
73
|
+
};
|
|
74
|
+
const stateScopeRef = { current: yield* Scope.make() };
|
|
75
|
+
const backgroundFibers = [];
|
|
76
|
+
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
77
|
+
const ctx = {
|
|
78
|
+
actorId,
|
|
79
|
+
state: machine.initial,
|
|
80
|
+
event: initEvent,
|
|
81
|
+
self,
|
|
82
|
+
system
|
|
83
|
+
};
|
|
84
|
+
const { effects: effectSlots } = machine._slots;
|
|
85
|
+
for (const bg of machine.backgroundEffects) {
|
|
86
|
+
const fiber = yield* bg.handler({
|
|
87
|
+
actorId,
|
|
88
|
+
state: machine.initial,
|
|
89
|
+
event: initEvent,
|
|
90
|
+
self,
|
|
91
|
+
effects: effectSlots,
|
|
92
|
+
system
|
|
93
|
+
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
94
|
+
backgroundFibers.push(fiber);
|
|
95
|
+
}
|
|
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);
|
|
110
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
111
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
|
|
112
|
+
yield* Ref.set(stoppedRef, true);
|
|
113
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
114
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
115
|
+
yield* setExit(ActorExit.Final(machine.initial));
|
|
116
|
+
return makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope);
|
|
117
|
+
}
|
|
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
|
+
}));
|
|
133
|
+
const stop = Effect.gen(function* () {
|
|
134
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
135
|
+
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
136
|
+
yield* Ref.set(stoppedRef, true);
|
|
137
|
+
yield* Fiber.interrupt(loopFiber);
|
|
138
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
139
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
140
|
+
yield* setExit(ActorExit.Stopped);
|
|
141
|
+
}).pipe(Effect.asVoid);
|
|
142
|
+
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
143
|
+
return {
|
|
144
|
+
...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
|
|
145
|
+
stop
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* Build the runtime handle (send/ask/getState/isStopped).
|
|
150
|
+
* Shared between initial-final and normal paths.
|
|
151
|
+
*/
|
|
152
|
+
const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope) => ({
|
|
153
|
+
send: (event) => Effect.gen(function* () {
|
|
154
|
+
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
155
|
+
_tag: "send",
|
|
156
|
+
event
|
|
157
|
+
});
|
|
158
|
+
}),
|
|
159
|
+
sendWait: (event) => Effect.gen(function* () {
|
|
160
|
+
if (!(yield* Ref.get(stoppedRef))) {
|
|
161
|
+
const done = yield* Deferred.make();
|
|
162
|
+
yield* Queue.offer(eventQueue, {
|
|
163
|
+
_tag: "sendWait",
|
|
164
|
+
event,
|
|
165
|
+
done
|
|
166
|
+
});
|
|
167
|
+
yield* Deferred.await(done);
|
|
168
|
+
}
|
|
169
|
+
}),
|
|
170
|
+
ask: (event) => Effect.gen(function* () {
|
|
171
|
+
if (yield* Ref.get(stoppedRef)) return yield* new NoReplyError({
|
|
172
|
+
actorId: "stopped",
|
|
173
|
+
eventTag: event._tag
|
|
174
|
+
});
|
|
175
|
+
const reply = yield* Deferred.make();
|
|
176
|
+
yield* Queue.offer(eventQueue, {
|
|
177
|
+
_tag: "ask",
|
|
178
|
+
event,
|
|
179
|
+
reply
|
|
180
|
+
});
|
|
181
|
+
return yield* Deferred.await(reply);
|
|
182
|
+
}),
|
|
183
|
+
getState: SubscriptionRef.get(stateRef),
|
|
184
|
+
stateRef,
|
|
185
|
+
isStopped: Ref.get(stoppedRef),
|
|
186
|
+
stop: Effect.void,
|
|
187
|
+
_queue: eventQueue,
|
|
188
|
+
_stoppedRef: stoppedRef,
|
|
189
|
+
exitDeferred,
|
|
190
|
+
actorScope
|
|
191
|
+
});
|
|
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);
|
|
196
|
+
const postponed = [];
|
|
197
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
198
|
+
const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
|
|
199
|
+
const event = queued.event;
|
|
200
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
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);
|
|
217
|
+
postponed.push({
|
|
218
|
+
_tag: "send",
|
|
219
|
+
event
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
shouldStop: 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
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (lifecycle?.onEvent !== void 0) yield* lifecycle.onEvent(currentState, event);
|
|
238
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
239
|
+
if (result.transitioned) yield* SubscriptionRef.set(stateRef, result.newState);
|
|
240
|
+
if (lifecycle?.onStateChange !== void 0 && result.transitioned) yield* lifecycle.onStateChange(result, event);
|
|
241
|
+
switch (queued._tag) {
|
|
242
|
+
case "call":
|
|
243
|
+
yield* Deferred.succeed(queued.reply, result);
|
|
244
|
+
break;
|
|
245
|
+
case "sendWait":
|
|
246
|
+
yield* Deferred.succeed(queued.done, void 0);
|
|
247
|
+
break;
|
|
248
|
+
case "ask":
|
|
249
|
+
if (result.hasReply) {
|
|
250
|
+
const replySchema = machine._replySchemas?.get(event._tag);
|
|
251
|
+
if (replySchema !== void 0) {
|
|
252
|
+
let decoded;
|
|
253
|
+
try {
|
|
254
|
+
decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
|
|
255
|
+
} catch (decodeError) {
|
|
256
|
+
yield* Deferred.die(queued.reply, decodeError);
|
|
257
|
+
return yield* Effect.die(decodeError);
|
|
258
|
+
}
|
|
259
|
+
yield* Deferred.succeed(queued.reply, decoded);
|
|
260
|
+
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
261
|
+
} else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
|
|
262
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
263
|
+
actorId,
|
|
264
|
+
eventTag: event._tag
|
|
265
|
+
}));
|
|
266
|
+
break;
|
|
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);
|
|
271
|
+
return {
|
|
272
|
+
shouldStop,
|
|
273
|
+
stateChanged: result.lifecycleRan,
|
|
274
|
+
result
|
|
275
|
+
};
|
|
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 = [];
|
|
282
|
+
let next = yield* Queue.poll(eventQueue);
|
|
283
|
+
while (Option.isSome(next)) {
|
|
284
|
+
remaining.push(next.value);
|
|
285
|
+
next = yield* Queue.poll(eventQueue);
|
|
286
|
+
}
|
|
287
|
+
for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
|
|
288
|
+
else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
|
|
289
|
+
actorId,
|
|
290
|
+
eventTag: entry.event._tag
|
|
291
|
+
})));
|
|
292
|
+
else if (entry._tag === "call") {
|
|
293
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
294
|
+
forkEffect(Deferred.succeed(entry.reply, {
|
|
295
|
+
newState: currentState,
|
|
296
|
+
previousState: currentState,
|
|
297
|
+
transitioned: false,
|
|
298
|
+
lifecycleRan: false,
|
|
299
|
+
isFinal: machine.finalStates.has(currentState._tag),
|
|
300
|
+
hasReply: false,
|
|
301
|
+
deferReply: false,
|
|
302
|
+
reply: void 0,
|
|
303
|
+
postponed: false
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
307
|
+
yield* setExit(exitReason);
|
|
308
|
+
});
|
|
309
|
+
while (true) {
|
|
310
|
+
const queued = yield* Queue.take(eventQueue);
|
|
311
|
+
if (queued._tag === "drain") {
|
|
312
|
+
yield* shutdown(ActorExit.Stopped);
|
|
313
|
+
yield* Deferred.succeed(queued.done, void 0);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const eventQueued = queued;
|
|
317
|
+
const processInner = processQueued(eventQueued);
|
|
318
|
+
const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
|
|
319
|
+
return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
|
|
320
|
+
}) : processInner).pipe(Effect.catchCause((cause) => {
|
|
321
|
+
if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
|
|
322
|
+
else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
|
|
323
|
+
else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
|
|
324
|
+
return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
325
|
+
}));
|
|
326
|
+
if (shouldStop) {
|
|
327
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
328
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
let drainTriggered = stateChanged;
|
|
332
|
+
while (drainTriggered && postponed.length > 0) {
|
|
333
|
+
drainTriggered = false;
|
|
334
|
+
const drained = postponed.splice(0);
|
|
335
|
+
for (const entry of drained) {
|
|
336
|
+
const drain = yield* processQueued(entry);
|
|
337
|
+
if (drain.shouldStop) {
|
|
338
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
339
|
+
yield* shutdown(ActorExit.Final(finalState));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (drain.stateChanged) drainTriggered = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
/** Settle all pending Deferreds in the postpone buffer on shutdown. */
|
|
348
|
+
const settlePostponed = (postponed, actorId, forkFn) => {
|
|
349
|
+
for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
|
|
350
|
+
actorId,
|
|
351
|
+
eventTag: entry.event._tag
|
|
352
|
+
})));
|
|
353
|
+
else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
|
|
354
|
+
postponed.length = 0;
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
export { createRuntime };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EffectsDef, GuardsDef, MachineContext } from "../slot.js";
|
|
2
|
-
import {
|
|
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
|
|
|
@@ -32,6 +32,7 @@ declare const runTransitionHandler: <S extends {
|
|
|
32
32
|
}, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, transition: Transition<S, E, GD, EFD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
|
|
33
33
|
newState: S;
|
|
34
34
|
hasReply: boolean;
|
|
35
|
+
deferReply: boolean;
|
|
35
36
|
reply: unknown;
|
|
36
37
|
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
37
38
|
/**
|
|
@@ -54,6 +55,7 @@ declare const executeTransition: <S extends {
|
|
|
54
55
|
transitioned: boolean;
|
|
55
56
|
reenter: boolean;
|
|
56
57
|
hasReply: boolean;
|
|
58
|
+
deferReply: boolean;
|
|
57
59
|
reply: unknown;
|
|
58
60
|
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>>>;
|
|
59
61
|
/**
|
|
@@ -66,6 +68,8 @@ interface ProcessEventHooks<S, E> {
|
|
|
66
68
|
readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
|
|
67
69
|
/** Called when a transition handler or spawn effect fails with a defect */
|
|
68
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>;
|
|
69
73
|
}
|
|
70
74
|
/**
|
|
71
75
|
* Error info for inspection hooks.
|
|
@@ -92,6 +96,8 @@ interface ProcessEventResult<S> {
|
|
|
92
96
|
readonly isFinal: boolean;
|
|
93
97
|
/** Whether the handler provided a reply (structural, not value-based) */
|
|
94
98
|
readonly hasReply: boolean;
|
|
99
|
+
/** Whether the handler deferred the reply to a spawn handler (Machine.deferReply) */
|
|
100
|
+
readonly deferReply: boolean;
|
|
95
101
|
/** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
|
|
96
102
|
readonly reply?: unknown;
|
|
97
103
|
/** Whether the event was postponed (buffered for retry after next state change) */
|
|
@@ -131,6 +137,7 @@ declare const processEventCore: <S extends {
|
|
|
131
137
|
lifecycleRan: boolean;
|
|
132
138
|
isFinal: boolean;
|
|
133
139
|
hasReply: boolean;
|
|
140
|
+
deferReply: boolean;
|
|
134
141
|
reply: unknown;
|
|
135
142
|
postponed: boolean;
|
|
136
143
|
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
@@ -143,7 +150,7 @@ declare const runSpawnEffects: <S extends {
|
|
|
143
150
|
readonly _tag: string;
|
|
144
151
|
}, E extends {
|
|
145
152
|
readonly _tag: string;
|
|
146
|
-
}, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.Closeable, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
153
|
+
}, R, GD extends GuardsDef, EFD extends EffectsDef>(machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>, 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>>;
|
|
147
154
|
/**
|
|
148
155
|
* Resolve which transition should fire for a given state and event.
|
|
149
156
|
* Uses indexed O(1) lookup. First matching transition wins.
|
|
@@ -161,14 +168,13 @@ declare const invalidateIndex: (machine: object) => void;
|
|
|
161
168
|
* Find all transitions matching a state/event pair.
|
|
162
169
|
* Returns empty array if no matches.
|
|
163
170
|
*
|
|
164
|
-
* Accepts both `Machine` and `BuiltMachine`.
|
|
165
171
|
* O(1) lookup after first access (index is lazily built).
|
|
166
172
|
*/
|
|
167
173
|
declare const findTransitions: <S extends {
|
|
168
174
|
readonly _tag: string;
|
|
169
175
|
}, E extends {
|
|
170
176
|
readonly _tag: string;
|
|
171
|
-
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(
|
|
177
|
+
}, R, GD extends GuardsDef = Record<string, never>, EFD extends EffectsDef = Record<string, never>>(machine: Machine<S, E, R, any, any, GD, EFD>, stateTag: string, eventTag: string) => ReadonlyArray<Transition<S, E, GD, EFD, R>>;
|
|
172
178
|
/**
|
|
173
179
|
* Find all spawn effects for a state.
|
|
174
180
|
* Returns empty array if no matches.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
|
|
2
|
-
import { BuiltMachine } from "../machine.js";
|
|
1
|
+
import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
|
|
3
2
|
import { Cause, Effect, Exit, Scope } from "effect";
|
|
4
3
|
//#region src/internal/transition.ts
|
|
5
4
|
/**
|
|
@@ -39,14 +38,22 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
|
|
|
39
38
|
};
|
|
40
39
|
const raw = transition.handler(handlerCtx);
|
|
41
40
|
const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
|
|
42
|
-
if (
|
|
41
|
+
if (isReplyResult(resolved)) return {
|
|
43
42
|
newState: resolved.state,
|
|
44
43
|
hasReply: true,
|
|
44
|
+
deferReply: false,
|
|
45
45
|
reply: resolved.reply
|
|
46
46
|
};
|
|
47
|
+
if (isDeferReplyResult(resolved)) return {
|
|
48
|
+
newState: resolved.state,
|
|
49
|
+
hasReply: false,
|
|
50
|
+
deferReply: true,
|
|
51
|
+
reply: void 0
|
|
52
|
+
};
|
|
47
53
|
return {
|
|
48
54
|
newState: resolved,
|
|
49
55
|
hasReply: false,
|
|
56
|
+
deferReply: false,
|
|
50
57
|
reply: void 0
|
|
51
58
|
};
|
|
52
59
|
});
|
|
@@ -68,14 +75,16 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
|
|
|
68
75
|
transitioned: false,
|
|
69
76
|
reenter: false,
|
|
70
77
|
hasReply: false,
|
|
78
|
+
deferReply: false,
|
|
71
79
|
reply: void 0
|
|
72
80
|
};
|
|
73
|
-
const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
|
|
81
|
+
const { newState, hasReply, deferReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
|
|
74
82
|
return {
|
|
75
83
|
newState,
|
|
76
84
|
transitioned: true,
|
|
77
85
|
reenter: transition.reenter === true,
|
|
78
86
|
hasReply,
|
|
87
|
+
deferReply,
|
|
79
88
|
reply
|
|
80
89
|
};
|
|
81
90
|
});
|
|
@@ -118,6 +127,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
118
127
|
lifecycleRan: false,
|
|
119
128
|
isFinal: false,
|
|
120
129
|
hasReply: false,
|
|
130
|
+
deferReply: false,
|
|
121
131
|
reply: void 0,
|
|
122
132
|
postponed: false
|
|
123
133
|
};
|
|
@@ -128,7 +138,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
128
138
|
stateScopeRef.current = yield* Scope.make();
|
|
129
139
|
if (hooks?.onTransition !== void 0) yield* hooks.onTransition(currentState, newState, event);
|
|
130
140
|
if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
|
|
131
|
-
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError);
|
|
141
|
+
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
|
|
132
142
|
}
|
|
133
143
|
return {
|
|
134
144
|
newState,
|
|
@@ -137,6 +147,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
137
147
|
lifecycleRan: runLifecycle,
|
|
138
148
|
isFinal: machine.finalStates.has(newState._tag),
|
|
139
149
|
hasReply: result.hasReply,
|
|
150
|
+
deferReply: result.deferReply,
|
|
140
151
|
reply: result.reply,
|
|
141
152
|
postponed: false
|
|
142
153
|
};
|
|
@@ -146,7 +157,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
146
157
|
*
|
|
147
158
|
* @internal
|
|
148
159
|
*/
|
|
149
|
-
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError) {
|
|
160
|
+
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
|
|
150
161
|
const spawnEffects = findSpawnEffects(machine, state._tag);
|
|
151
162
|
const ctx = {
|
|
152
163
|
actorId,
|
|
@@ -157,6 +168,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
157
168
|
};
|
|
158
169
|
const { effects: effectSlots } = machine._slots;
|
|
159
170
|
const reportError = onError;
|
|
171
|
+
const defectSignal = onSpawnDefect;
|
|
160
172
|
for (const spawnEffect of spawnEffects) {
|
|
161
173
|
const effect = spawnEffect.handler({
|
|
162
174
|
actorId,
|
|
@@ -167,13 +179,14 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
167
179
|
system
|
|
168
180
|
}).pipe(Effect.provideService(machine.Context, ctx), Effect.catchCause((cause) => {
|
|
169
181
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
170
|
-
|
|
171
|
-
return reportError({
|
|
182
|
+
const report = reportError !== void 0 ? reportError({
|
|
172
183
|
phase: "spawn",
|
|
173
184
|
state,
|
|
174
185
|
event,
|
|
175
186
|
cause
|
|
176
|
-
})
|
|
187
|
+
}) : Effect.void;
|
|
188
|
+
const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
|
|
189
|
+
return report.pipe(Effect.andThen(signal), Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
177
190
|
}));
|
|
178
191
|
yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
|
|
179
192
|
}
|
|
@@ -246,11 +259,10 @@ const getIndex = (machine) => {
|
|
|
246
259
|
* Find all transitions matching a state/event pair.
|
|
247
260
|
* Returns empty array if no matches.
|
|
248
261
|
*
|
|
249
|
-
* Accepts both `Machine` and `BuiltMachine`.
|
|
250
262
|
* O(1) lookup after first access (index is lazily built).
|
|
251
263
|
*/
|
|
252
|
-
const findTransitions = (
|
|
253
|
-
const index = getIndex(
|
|
264
|
+
const findTransitions = (machine, stateTag, eventTag) => {
|
|
265
|
+
const index = getIndex(machine);
|
|
254
266
|
const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
|
|
255
267
|
if (specific.length > 0) return specific;
|
|
256
268
|
return index.transitions.get("*")?.get(eventTag) ?? [];
|
package/dist/internal/utils.d.ts
CHANGED
|
@@ -23,15 +23,51 @@ type InstanceOf<C> = C extends ((...args: unknown[]) => infer R) ? R : never;
|
|
|
23
23
|
type TaggedConstructor<T extends {
|
|
24
24
|
readonly _tag: string;
|
|
25
25
|
}> = (args: Omit<T, "_tag">) => T;
|
|
26
|
+
declare const ReplyResultSymbol: unique symbol;
|
|
27
|
+
type ReplyResultSymbol = typeof ReplyResultSymbol;
|
|
26
28
|
/**
|
|
27
|
-
*
|
|
29
|
+
* Branded reply result from a transition handler.
|
|
30
|
+
* Created via `Machine.reply(state, value)`.
|
|
28
31
|
*/
|
|
29
|
-
|
|
30
|
-
interface TransitionReply<State> {
|
|
32
|
+
interface ReplyResult<State, Reply> {
|
|
31
33
|
readonly state: State;
|
|
32
|
-
readonly reply:
|
|
34
|
+
readonly reply: Reply;
|
|
35
|
+
readonly [ReplyResultSymbol]: true;
|
|
33
36
|
}
|
|
34
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Create a reply result for ask-bearing event handlers.
|
|
39
|
+
*/
|
|
40
|
+
declare const makeReply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
|
|
41
|
+
/**
|
|
42
|
+
* Type guard for ReplyResult (symbol-based, replaces duck-typing).
|
|
43
|
+
*/
|
|
44
|
+
declare const isReplyResult: (value: unknown) => value is ReplyResult<unknown, unknown>;
|
|
45
|
+
declare const DeferReplySymbol: unique symbol;
|
|
46
|
+
type DeferReplySymbol = typeof DeferReplySymbol;
|
|
47
|
+
/**
|
|
48
|
+
* Branded deferred reply result from a transition handler.
|
|
49
|
+
* Signals that the reply will be settled later by `self.reply()` in a spawn handler.
|
|
50
|
+
* Created via `Machine.deferReply(state)`.
|
|
51
|
+
*/
|
|
52
|
+
interface DeferReplyResult<State> {
|
|
53
|
+
readonly state: State;
|
|
54
|
+
readonly [DeferReplySymbol]: true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create a deferred reply result. Handler returns this to signal
|
|
58
|
+
* "spawn handler will call self.reply(value) later".
|
|
59
|
+
*/
|
|
60
|
+
declare const makeDeferReply: <State>(state: State) => DeferReplyResult<State>;
|
|
61
|
+
/**
|
|
62
|
+
* Type guard for DeferReplyResult.
|
|
63
|
+
*/
|
|
64
|
+
declare const isDeferReplyResult: (value: unknown) => value is DeferReplyResult<unknown>;
|
|
65
|
+
/**
|
|
66
|
+
* Transition handler result.
|
|
67
|
+
* - When Reply is `never`: handler returns plain State (no reply allowed)
|
|
68
|
+
* - When Reply is concrete: handler must return ReplyResult via Machine.reply()
|
|
69
|
+
*/
|
|
70
|
+
type TransitionResult<State, R, Reply = never> = [Reply] extends [never] ? State | Effect.Effect<State, never, R> : ReplyResult<State, Reply> | DeferReplyResult<State> | Effect.Effect<ReplyResult<State, Reply> | DeferReplyResult<State>, never, R>;
|
|
35
71
|
/**
|
|
36
72
|
* Internal event tags used for lifecycle effect contexts.
|
|
37
73
|
* Prefixed with $ to distinguish from user events.
|
|
@@ -62,4 +98,4 @@ declare const isEffect: (value: unknown) => value is Effect.Effect<unknown, unkn
|
|
|
62
98
|
*/
|
|
63
99
|
declare const stubSystem: ActorSystem;
|
|
64
100
|
//#endregion
|
|
65
|
-
export { ArgsOf, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, TagOf, TaggedConstructor,
|
|
101
|
+
export { ArgsOf, DeferReplyResult, DeferReplySymbol, INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, InstanceOf, ReplyResult, ReplyResultSymbol, TagOf, TaggedConstructor, TransitionResult, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
|
package/dist/internal/utils.js
CHANGED
|
@@ -4,6 +4,32 @@ import { Effect, Stream } from "effect";
|
|
|
4
4
|
* Internal utilities for effect-machine.
|
|
5
5
|
* @internal
|
|
6
6
|
*/
|
|
7
|
+
const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
|
|
8
|
+
/**
|
|
9
|
+
* Create a reply result for ask-bearing event handlers.
|
|
10
|
+
*/
|
|
11
|
+
const makeReply = (state, reply) => ({
|
|
12
|
+
state,
|
|
13
|
+
reply,
|
|
14
|
+
[ReplyResultSymbol]: true
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Type guard for ReplyResult (symbol-based, replaces duck-typing).
|
|
18
|
+
*/
|
|
19
|
+
const isReplyResult = (value) => value !== null && typeof value === "object" && ReplyResultSymbol in value;
|
|
20
|
+
const DeferReplySymbol = Symbol.for("effect-machine/DeferReply");
|
|
21
|
+
/**
|
|
22
|
+
* Create a deferred reply result. Handler returns this to signal
|
|
23
|
+
* "spawn handler will call self.reply(value) later".
|
|
24
|
+
*/
|
|
25
|
+
const makeDeferReply = (state) => ({
|
|
26
|
+
state,
|
|
27
|
+
[DeferReplySymbol]: true
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Type guard for DeferReplyResult.
|
|
31
|
+
*/
|
|
32
|
+
const isDeferReplyResult = (value) => value !== null && typeof value === "object" && DeferReplySymbol in value;
|
|
7
33
|
/**
|
|
8
34
|
* Internal event tags used for lifecycle effect contexts.
|
|
9
35
|
* Prefixed with $ to distinguish from user events.
|
|
@@ -46,4 +72,4 @@ const stubSystem = {
|
|
|
46
72
|
subscribe: () => () => {}
|
|
47
73
|
};
|
|
48
74
|
//#endregion
|
|
49
|
-
export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
|
|
75
|
+
export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
|