effect-machine 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -173
- package/dist/actor.d.ts +65 -22
- package/dist/actor.js +172 -78
- package/dist/atom.d.ts +28 -3
- package/dist/atom.js +20 -2
- package/dist/cluster/entity-machine.d.ts +9 -3
- package/dist/cluster/entity-machine.js +8 -8
- package/dist/cluster/index.d.ts +2 -2
- package/dist/cluster/to-entity.d.ts +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/inspection.d.ts +31 -3
- package/dist/inspection.js +21 -0
- package/dist/internal/inspection.d.ts +1 -1
- package/dist/internal/inspection.js +23 -1
- package/dist/internal/machine-definition.d.ts +1 -0
- package/dist/internal/machine-initialization.d.ts +21 -0
- package/dist/internal/machine-initialization.js +27 -0
- package/dist/internal/runtime.d.ts +15 -1
- package/dist/internal/runtime.js +67 -26
- package/dist/internal/transition.d.ts +51 -3
- package/dist/internal/transition.js +216 -38
- package/dist/machine.d.ts +151 -40
- package/dist/machine.js +139 -40
- package/dist/supervision.d.ts +3 -2
- package/dist/supervision.js +3 -2
- package/dist/testing.d.ts +39 -67
- package/dist/testing.js +13 -52
- package/package.json +3 -2
|
@@ -11,14 +11,21 @@ import { Cause, Effect, Exit, Scope } from "effect";
|
|
|
11
11
|
*
|
|
12
12
|
* @internal
|
|
13
13
|
*/
|
|
14
|
-
const completeTransition = (transition, resolved) => {
|
|
14
|
+
const completeTransition = (currentState, event, transition, resolved) => {
|
|
15
15
|
if (isReplyResult(resolved)) return {
|
|
16
16
|
newState: resolved.state,
|
|
17
17
|
transitioned: true,
|
|
18
18
|
reenter: transition.reenter === true,
|
|
19
19
|
hasReply: true,
|
|
20
20
|
deferReply: false,
|
|
21
|
-
reply: resolved.reply
|
|
21
|
+
reply: resolved.reply,
|
|
22
|
+
transition,
|
|
23
|
+
steps: [{
|
|
24
|
+
previousState: currentState,
|
|
25
|
+
newState: resolved.state,
|
|
26
|
+
event,
|
|
27
|
+
transition
|
|
28
|
+
}]
|
|
22
29
|
};
|
|
23
30
|
if (isDeferReplyResult(resolved)) return {
|
|
24
31
|
newState: resolved.state,
|
|
@@ -26,7 +33,14 @@ const completeTransition = (transition, resolved) => {
|
|
|
26
33
|
reenter: transition.reenter === true,
|
|
27
34
|
hasReply: false,
|
|
28
35
|
deferReply: true,
|
|
29
|
-
reply: void 0
|
|
36
|
+
reply: void 0,
|
|
37
|
+
transition,
|
|
38
|
+
steps: [{
|
|
39
|
+
previousState: currentState,
|
|
40
|
+
newState: resolved.state,
|
|
41
|
+
event,
|
|
42
|
+
transition
|
|
43
|
+
}]
|
|
30
44
|
};
|
|
31
45
|
return {
|
|
32
46
|
newState: resolved,
|
|
@@ -34,7 +48,14 @@ const completeTransition = (transition, resolved) => {
|
|
|
34
48
|
reenter: transition.reenter === true,
|
|
35
49
|
hasReply: false,
|
|
36
50
|
deferReply: false,
|
|
37
|
-
reply: void 0
|
|
51
|
+
reply: void 0,
|
|
52
|
+
transition,
|
|
53
|
+
steps: [{
|
|
54
|
+
previousState: currentState,
|
|
55
|
+
newState: resolved,
|
|
56
|
+
event,
|
|
57
|
+
transition
|
|
58
|
+
}]
|
|
38
59
|
};
|
|
39
60
|
};
|
|
40
61
|
/**
|
|
@@ -50,8 +71,10 @@ const completeTransition = (transition, resolved) => {
|
|
|
50
71
|
*/
|
|
51
72
|
const executeTransition = (machine, currentState, event) => Effect.suspend(() => {
|
|
52
73
|
const result = executeTransitionImmediate(machine, currentState, event);
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
let first;
|
|
75
|
+
if (isEffect(result)) first = result;
|
|
76
|
+
else first = Effect.succeed(result);
|
|
77
|
+
return first.pipe(Effect.flatMap((initialResult) => stabilizeTransition(machine, currentState, event, initialResult)));
|
|
55
78
|
});
|
|
56
79
|
/**
|
|
57
80
|
* Execute a transition without adding an Effect boundary for a synchronous handler.
|
|
@@ -59,29 +82,98 @@ const executeTransition = (machine, currentState, event) => Effect.suspend(() =>
|
|
|
59
82
|
*
|
|
60
83
|
* @internal
|
|
61
84
|
*/
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
if (transition === void 0) return {
|
|
65
|
-
newState: currentState,
|
|
66
|
-
transitioned: false,
|
|
67
|
-
reenter: false,
|
|
68
|
-
hasReply: false,
|
|
69
|
-
deferReply: false,
|
|
70
|
-
reply: void 0
|
|
71
|
-
};
|
|
72
|
-
const handlerCtx = {
|
|
73
|
-
state: currentState,
|
|
74
|
-
event
|
|
75
|
-
};
|
|
76
|
-
let raw;
|
|
85
|
+
const executeTransitionCandidatesImmediate = (currentState, event, candidates, hooks) => {
|
|
86
|
+
let resolution;
|
|
77
87
|
try {
|
|
78
|
-
|
|
88
|
+
resolution = evaluateTransitions(candidates, currentState, event);
|
|
79
89
|
} catch (defect) {
|
|
80
90
|
return Effect.die(defect);
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
const executeResolved = (resolved) => {
|
|
93
|
+
const transition = resolved.transition;
|
|
94
|
+
if (transition === void 0) {
|
|
95
|
+
const unhandled = {
|
|
96
|
+
newState: currentState,
|
|
97
|
+
transitioned: false,
|
|
98
|
+
reenter: false,
|
|
99
|
+
hasReply: false,
|
|
100
|
+
deferReply: false,
|
|
101
|
+
reply: void 0,
|
|
102
|
+
transition: void 0,
|
|
103
|
+
steps: []
|
|
104
|
+
};
|
|
105
|
+
if (hooks?.onGuard === void 0 || resolved.evaluations.length === 0) return unhandled;
|
|
106
|
+
return Effect.forEach(resolved.evaluations, hooks.onGuard, { discard: true }).pipe(Effect.as(unhandled));
|
|
107
|
+
}
|
|
108
|
+
const runHandler = () => {
|
|
109
|
+
const handlerCtx = {
|
|
110
|
+
state: currentState,
|
|
111
|
+
event
|
|
112
|
+
};
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = transition.handler(handlerCtx);
|
|
116
|
+
} catch (defect) {
|
|
117
|
+
return Effect.die(defect);
|
|
118
|
+
}
|
|
119
|
+
if (isEffect(raw)) return raw.pipe(Effect.map((value) => completeTransition(currentState, event, transition, value)));
|
|
120
|
+
return completeTransition(currentState, event, transition, raw);
|
|
121
|
+
};
|
|
122
|
+
const runInspectedHandler = () => {
|
|
123
|
+
if (hooks?.onOperation === void 0) return runHandler();
|
|
124
|
+
return hooks.onOperation({
|
|
125
|
+
operation: transition.handler.name || "<inline>",
|
|
126
|
+
state: currentState,
|
|
127
|
+
event
|
|
128
|
+
}).pipe(Effect.andThen(Effect.suspend(() => {
|
|
129
|
+
const result = runHandler();
|
|
130
|
+
if (isEffect(result)) return result;
|
|
131
|
+
return Effect.succeed(result);
|
|
132
|
+
})));
|
|
133
|
+
};
|
|
134
|
+
if (hooks?.onGuard === void 0 || resolved.evaluations.length === 0) return runInspectedHandler();
|
|
135
|
+
return Effect.forEach(resolved.evaluations, hooks.onGuard, { discard: true }).pipe(Effect.andThen(Effect.suspend(() => {
|
|
136
|
+
const result = runInspectedHandler();
|
|
137
|
+
if (isEffect(result)) return result;
|
|
138
|
+
return Effect.succeed(result);
|
|
139
|
+
})));
|
|
140
|
+
};
|
|
141
|
+
if (!isEffect(resolution)) return executeResolved(resolution);
|
|
142
|
+
return resolution.pipe(Effect.flatMap((resolved) => {
|
|
143
|
+
const result = executeResolved(resolved);
|
|
144
|
+
if (isEffect(result)) return result;
|
|
145
|
+
return Effect.succeed(result);
|
|
146
|
+
}));
|
|
84
147
|
};
|
|
148
|
+
const executeTransitionImmediate = (machine, currentState, event, hooks) => executeTransitionCandidatesImmediate(currentState, event, machine._findTransitions(currentState._tag, event._tag), hooks);
|
|
149
|
+
const stabilizeTransition = (machine, initialState, event, result, hooks) => Effect.gen(function* () {
|
|
150
|
+
const steps = [...result.steps];
|
|
151
|
+
let stableState = result.newState;
|
|
152
|
+
let lifecycleRequired = result.reenter || stableState._tag !== initialState._tag;
|
|
153
|
+
let depth = 0;
|
|
154
|
+
while (true) {
|
|
155
|
+
const candidates = machine._findImmediateTransitions(stableState._tag);
|
|
156
|
+
if (candidates.length === 0) break;
|
|
157
|
+
depth += 1;
|
|
158
|
+
if (depth > 100) return yield* Effect.die(/* @__PURE__ */ new Error("Immediate transition limit exceeded. Check for an eventless loop."));
|
|
159
|
+
const immediate = executeTransitionCandidatesImmediate(stableState, event, candidates, hooks);
|
|
160
|
+
let next;
|
|
161
|
+
if (isEffect(immediate)) next = yield* immediate;
|
|
162
|
+
else next = immediate;
|
|
163
|
+
if (!next.transitioned) break;
|
|
164
|
+
if (next.reenter || next.newState._tag !== stableState._tag) lifecycleRequired = true;
|
|
165
|
+
steps.push(...next.steps);
|
|
166
|
+
stableState = next.newState;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
...result,
|
|
170
|
+
newState: stableState,
|
|
171
|
+
transitioned: steps.length > 0,
|
|
172
|
+
reenter: lifecycleRequired,
|
|
173
|
+
transition: steps.at(-1)?.transition,
|
|
174
|
+
steps
|
|
175
|
+
};
|
|
176
|
+
});
|
|
85
177
|
/**
|
|
86
178
|
* Check if an event should be postponed in the current state.
|
|
87
179
|
* @internal
|
|
@@ -99,12 +191,12 @@ const shouldPostpone = (machine, stateTag, eventTag) => machine._shouldPostpone(
|
|
|
99
191
|
*
|
|
100
192
|
* @internal
|
|
101
193
|
*/
|
|
102
|
-
const processEventCore = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks) => Effect.suspend(() => {
|
|
103
|
-
const processed = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
194
|
+
const processEventCore = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation = 0) => Effect.suspend(() => {
|
|
195
|
+
const processed = processEventCoreImmediate(machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation);
|
|
104
196
|
if (isEffect(processed)) return processed;
|
|
105
197
|
return Effect.succeed(processed);
|
|
106
198
|
});
|
|
107
|
-
const completeProcessedEvent = (machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks) => {
|
|
199
|
+
const completeProcessedEvent = (machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks, generation = 0) => {
|
|
108
200
|
if (!result.transitioned) return {
|
|
109
201
|
newState: currentState,
|
|
110
202
|
previousState: currentState,
|
|
@@ -114,7 +206,8 @@ const completeProcessedEvent = (machine, currentState, event, result, self, stat
|
|
|
114
206
|
hasReply: false,
|
|
115
207
|
deferReply: false,
|
|
116
208
|
reply: void 0,
|
|
117
|
-
postponed: false
|
|
209
|
+
postponed: false,
|
|
210
|
+
transitions: []
|
|
118
211
|
};
|
|
119
212
|
const newState = result.newState;
|
|
120
213
|
const runLifecycle = newState._tag !== currentState._tag || result.reenter;
|
|
@@ -127,22 +220,59 @@ const completeProcessedEvent = (machine, currentState, event, result, self, stat
|
|
|
127
220
|
hasReply: result.hasReply,
|
|
128
221
|
deferReply: result.deferReply,
|
|
129
222
|
reply: result.reply,
|
|
130
|
-
postponed: false
|
|
223
|
+
postponed: false,
|
|
224
|
+
transitions: result.steps
|
|
131
225
|
};
|
|
132
|
-
|
|
226
|
+
const observeTransitions = hooks?.onTransition;
|
|
227
|
+
if (!runLifecycle) {
|
|
228
|
+
if (observeTransitions === void 0 || result.steps.length === 0) return processed;
|
|
229
|
+
return Effect.forEach(result.steps, (step) => observeTransitions(step.previousState, step.newState, step.event), { discard: true }).pipe(Effect.as(processed));
|
|
230
|
+
}
|
|
133
231
|
return Effect.gen(function* () {
|
|
134
232
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
135
233
|
stateScopeRef.current = yield* Scope.make();
|
|
136
|
-
if (
|
|
234
|
+
if (observeTransitions !== void 0) yield* Effect.forEach(result.steps, (step) => observeTransitions(step.previousState, step.newState, step.event), { discard: true });
|
|
137
235
|
if (hooks?.onSpawnEffect !== void 0) yield* hooks.onSpawnEffect(newState);
|
|
138
|
-
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect);
|
|
236
|
+
yield* runSpawnEffects(machine, newState, { _tag: INTERNAL_ENTER_EVENT }, self, stateScopeRef.current, system, actorId, hooks?.onError, hooks?.onSpawnDefect, generation);
|
|
139
237
|
return processed;
|
|
140
238
|
});
|
|
141
239
|
};
|
|
142
240
|
/** @internal */
|
|
143
|
-
const processEventCoreImmediate = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks) => {
|
|
144
|
-
const execution = executeTransitionImmediate(machine, currentState, event);
|
|
145
|
-
const complete = (result) =>
|
|
241
|
+
const processEventCoreImmediate = (machine, currentState, event, self, stateScopeRef, system, actorId, hooks, generation = 0) => {
|
|
242
|
+
const execution = executeTransitionImmediate(machine, currentState, event, hooks);
|
|
243
|
+
const complete = (result) => {
|
|
244
|
+
if (machine._findImmediateTransitions(result.newState._tag).length === 0) return completeProcessedEvent(machine, currentState, event, result, self, stateScopeRef, system, actorId, hooks, generation);
|
|
245
|
+
return Effect.gen(function* () {
|
|
246
|
+
const steps = [...result.steps];
|
|
247
|
+
let stableState = result.newState;
|
|
248
|
+
let lifecycleRequired = result.reenter || stableState._tag !== currentState._tag;
|
|
249
|
+
let depth = 0;
|
|
250
|
+
while (true) {
|
|
251
|
+
const candidates = machine._findImmediateTransitions(stableState._tag);
|
|
252
|
+
if (candidates.length === 0) break;
|
|
253
|
+
depth += 1;
|
|
254
|
+
if (depth > 100) return yield* Effect.die(/* @__PURE__ */ new Error("Immediate transition limit exceeded. Check for an eventless loop."));
|
|
255
|
+
const immediate = executeTransitionCandidatesImmediate(stableState, event, candidates, hooks);
|
|
256
|
+
let next;
|
|
257
|
+
if (isEffect(immediate)) next = yield* immediate;
|
|
258
|
+
else next = immediate;
|
|
259
|
+
if (!next.transitioned) break;
|
|
260
|
+
if (next.reenter || next.newState._tag !== stableState._tag) lifecycleRequired = true;
|
|
261
|
+
steps.push(...next.steps);
|
|
262
|
+
stableState = next.newState;
|
|
263
|
+
}
|
|
264
|
+
const processed = completeProcessedEvent(machine, currentState, event, {
|
|
265
|
+
...result,
|
|
266
|
+
newState: stableState,
|
|
267
|
+
transitioned: steps.length > 0,
|
|
268
|
+
reenter: lifecycleRequired,
|
|
269
|
+
transition: steps.at(-1)?.transition,
|
|
270
|
+
steps
|
|
271
|
+
}, self, stateScopeRef, system, actorId, hooks, generation);
|
|
272
|
+
if (isEffect(processed)) return yield* processed;
|
|
273
|
+
return processed;
|
|
274
|
+
});
|
|
275
|
+
};
|
|
146
276
|
if (!isEffect(execution)) return complete(execution);
|
|
147
277
|
return execution.pipe(Effect.catchCause((cause) => {
|
|
148
278
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
@@ -165,13 +295,14 @@ const processEventCoreImmediate = (machine, currentState, event, self, stateScop
|
|
|
165
295
|
*
|
|
166
296
|
* @internal
|
|
167
297
|
*/
|
|
168
|
-
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect) {
|
|
298
|
+
const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (machine, state, event, self, stateScope, system, actorId, onError, onSpawnDefect, generation = 0) {
|
|
169
299
|
const spawnEffects = machine._findSpawnEffects(state._tag);
|
|
170
300
|
const reportError = onError;
|
|
171
301
|
const defectSignal = onSpawnDefect;
|
|
172
302
|
for (const spawnEffect of spawnEffects) {
|
|
173
303
|
const effect = spawnEffect.handler({
|
|
174
304
|
actorId,
|
|
305
|
+
generation,
|
|
175
306
|
state,
|
|
176
307
|
event,
|
|
177
308
|
self,
|
|
@@ -197,7 +328,54 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
197
328
|
* Uses indexed O(1) lookup. First matching transition wins.
|
|
198
329
|
*/
|
|
199
330
|
const resolveTransition = (machine, currentState, event) => {
|
|
200
|
-
|
|
331
|
+
const resolution = evaluateTransitions(machine._findTransitions(currentState._tag, event._tag), currentState, event);
|
|
332
|
+
if (isEffect(resolution)) return Effect.runSync(Effect.die("Effect guards require actor.can(event). actor.client.canSync(event) is synchronous."));
|
|
333
|
+
return resolution.transition;
|
|
334
|
+
};
|
|
335
|
+
/** Resolve a transition with pure or Effect guards. */
|
|
336
|
+
const resolveTransitionEffect = (machine, currentState, event) => {
|
|
337
|
+
const resolution = evaluateTransitions(machine._findTransitions(currentState._tag, event._tag), currentState, event);
|
|
338
|
+
if (isEffect(resolution)) return resolution.pipe(Effect.map((value) => value.transition));
|
|
339
|
+
return Effect.succeed(resolution.transition);
|
|
340
|
+
};
|
|
341
|
+
const evaluateTransitions = (candidates, currentState, event) => {
|
|
342
|
+
const ctx = {
|
|
343
|
+
state: currentState,
|
|
344
|
+
event
|
|
345
|
+
};
|
|
346
|
+
const loop = (index, evaluations) => {
|
|
347
|
+
const candidate = candidates[index];
|
|
348
|
+
if (candidate === void 0) return {
|
|
349
|
+
transition: void 0,
|
|
350
|
+
evaluations
|
|
351
|
+
};
|
|
352
|
+
if (candidate.guard === void 0) return {
|
|
353
|
+
transition: candidate,
|
|
354
|
+
evaluations
|
|
355
|
+
};
|
|
356
|
+
const guardName = candidate.guard.name || "<inline>";
|
|
357
|
+
const result = candidate.guard(ctx);
|
|
358
|
+
const continueWith = (passed) => {
|
|
359
|
+
const nextEvaluations = [...evaluations, {
|
|
360
|
+
guard: guardName,
|
|
361
|
+
state: currentState,
|
|
362
|
+
event,
|
|
363
|
+
result: passed
|
|
364
|
+
}];
|
|
365
|
+
if (passed) return {
|
|
366
|
+
transition: candidate,
|
|
367
|
+
evaluations: nextEvaluations
|
|
368
|
+
};
|
|
369
|
+
return loop(index + 1, nextEvaluations);
|
|
370
|
+
};
|
|
371
|
+
if (!isEffect(result)) return continueWith(result);
|
|
372
|
+
return result.pipe(Effect.flatMap((passed) => {
|
|
373
|
+
const next = continueWith(passed);
|
|
374
|
+
if (isEffect(next)) return next;
|
|
375
|
+
return Effect.succeed(next);
|
|
376
|
+
}));
|
|
377
|
+
};
|
|
378
|
+
return loop(0, []);
|
|
201
379
|
};
|
|
202
380
|
//#endregion
|
|
203
|
-
export { executeTransition, executeTransitionImmediate, processEventCore, processEventCoreImmediate, resolveTransition, runSpawnEffects, shouldPostpone };
|
|
381
|
+
export { executeTransition, executeTransitionImmediate, processEventCore, processEventCoreImmediate, resolveTransition, resolveTransitionEffect, runSpawnEffects, shouldPostpone };
|