effect-machine 0.1.0 → 0.2.1
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 +17 -0
- package/package.json +1 -1
- package/src/actor.ts +544 -455
- package/src/cluster/entity-machine.ts +81 -82
- package/src/index.ts +1 -0
- package/src/inspection.ts +17 -0
- package/src/internal/inspection.ts +18 -0
- package/src/internal/transition.ts +150 -94
- package/src/machine.ts +139 -71
- package/src/persistence/adapter.ts +5 -4
- package/src/persistence/adapters/in-memory.ts +201 -182
- package/src/persistence/persistent-actor.ts +582 -386
- package/src/testing.ts +92 -98
- package/tsconfig.json +2 -5
|
@@ -41,6 +41,8 @@ export interface EntityMachineOptions<S, E> {
|
|
|
41
41
|
* Effect.log(`Transition: ${from._tag} -> ${to._tag}`),
|
|
42
42
|
* onSpawnEffect: (state) =>
|
|
43
43
|
* Effect.log(`Running spawn effects for ${state._tag}`),
|
|
44
|
+
* onError: ({ phase, state }) =>
|
|
45
|
+
* Effect.log(`Defect in ${phase} at ${state._tag}`),
|
|
44
46
|
* },
|
|
45
47
|
* })
|
|
46
48
|
* ```
|
|
@@ -52,7 +54,7 @@ export interface EntityMachineOptions<S, E> {
|
|
|
52
54
|
* Process a single event through the machine using shared core.
|
|
53
55
|
* Returns the new state after processing.
|
|
54
56
|
*/
|
|
55
|
-
const processEvent = <
|
|
57
|
+
const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* <
|
|
56
58
|
S extends { readonly _tag: string },
|
|
57
59
|
E extends { readonly _tag: string },
|
|
58
60
|
R,
|
|
@@ -65,27 +67,19 @@ const processEvent = <
|
|
|
65
67
|
self: MachineRef<E>,
|
|
66
68
|
stateScopeRef: { current: Scope.CloseableScope },
|
|
67
69
|
hooks?: ProcessEventHooks<S, E>,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// Update state ref if transition occurred
|
|
83
|
-
if (result.transitioned) {
|
|
84
|
-
yield* Ref.set(stateRef, result.newState);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result.newState;
|
|
88
|
-
});
|
|
70
|
+
) {
|
|
71
|
+
const currentState = yield* Ref.get(stateRef);
|
|
72
|
+
|
|
73
|
+
// Process event using shared core
|
|
74
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, hooks);
|
|
75
|
+
|
|
76
|
+
// Update state ref if transition occurred
|
|
77
|
+
if (result.transitioned) {
|
|
78
|
+
yield* Ref.set(stateRef, result.newState);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result.newState;
|
|
82
|
+
});
|
|
89
83
|
|
|
90
84
|
/**
|
|
91
85
|
* Create an Entity layer that wires a machine to handle RPC calls.
|
|
@@ -137,66 +131,71 @@ export const EntityMachine = {
|
|
|
137
131
|
machine: Machine<S, E, R, Record<string, never>, Record<string, never>, GD, EFD>,
|
|
138
132
|
options?: EntityMachineOptions<S, E>,
|
|
139
133
|
): Layer.Layer<never, never, R> => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
options
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
134
|
+
const layer = Effect.fn("effect-machine.cluster.layer")(function* () {
|
|
135
|
+
// Get entity ID from context if available
|
|
136
|
+
const entityId = yield* Effect.serviceOption(Entity.CurrentAddress).pipe(
|
|
137
|
+
Effect.map((opt) => (opt._tag === "Some" ? opt.value.entityId : "")),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Initialize state - use provided initializer or machine's initial state
|
|
141
|
+
const initialState =
|
|
142
|
+
options?.initializeState !== undefined
|
|
143
|
+
? options.initializeState(entityId)
|
|
144
|
+
: machine.initial;
|
|
145
|
+
|
|
146
|
+
// Create self reference for sending events back to machine
|
|
147
|
+
const internalQueue = yield* Queue.unbounded<E>();
|
|
148
|
+
const self: MachineRef<E> = {
|
|
149
|
+
send: Effect.fn("effect-machine.cluster.self.send")(function* (event: E) {
|
|
150
|
+
yield* Queue.offer(internalQueue, event);
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Create state ref
|
|
155
|
+
const stateRef = yield* Ref.make<S>(initialState);
|
|
156
|
+
|
|
157
|
+
// Create state scope for spawn effects
|
|
158
|
+
const stateScopeRef: { current: Scope.CloseableScope } = {
|
|
159
|
+
current: yield* Scope.make(),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Use $init event for initial lifecycle
|
|
163
|
+
const initEvent = { _tag: "$init" } as E;
|
|
164
|
+
|
|
165
|
+
// Run initial spawn effects
|
|
166
|
+
yield* runSpawnEffects(
|
|
167
|
+
machine,
|
|
168
|
+
initialState,
|
|
169
|
+
initEvent,
|
|
170
|
+
self,
|
|
171
|
+
stateScopeRef.current,
|
|
172
|
+
options?.hooks?.onError,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Process internal events in background
|
|
176
|
+
const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
|
|
177
|
+
const event = yield* Queue.take(internalQueue);
|
|
178
|
+
yield* processEvent(machine, stateRef, event, self, stateScopeRef, options?.hooks);
|
|
179
|
+
});
|
|
180
|
+
yield* Effect.forkScoped(Effect.forever(runInternalEvent()));
|
|
181
|
+
|
|
182
|
+
// Return handlers matching the Entity's RPC protocol
|
|
183
|
+
// The actual types are inferred from the entity definition
|
|
184
|
+
return entity.of({
|
|
185
|
+
Send: (envelope: { payload: { event: E } }) =>
|
|
186
|
+
processEvent(
|
|
187
|
+
machine,
|
|
188
|
+
stateRef,
|
|
189
|
+
envelope.payload.event,
|
|
190
|
+
self,
|
|
191
|
+
stateScopeRef,
|
|
192
|
+
options?.hooks,
|
|
180
193
|
),
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
processEvent(
|
|
188
|
-
machine,
|
|
189
|
-
stateRef,
|
|
190
|
-
envelope.payload.event,
|
|
191
|
-
self,
|
|
192
|
-
stateScopeRef,
|
|
193
|
-
options?.hooks,
|
|
194
|
-
),
|
|
195
|
-
|
|
196
|
-
GetState: () => Ref.get(stateRef),
|
|
197
|
-
// Entity.of expects handlers matching Rpcs type param - dynamic construction requires cast
|
|
198
|
-
} as unknown as Parameters<typeof entity.of>[0]);
|
|
199
|
-
}),
|
|
200
|
-
) as unknown as Layer.Layer<never, never, R>;
|
|
194
|
+
|
|
195
|
+
GetState: () => Ref.get(stateRef),
|
|
196
|
+
// Entity.of expects handlers matching Rpcs type param - dynamic construction requires cast
|
|
197
|
+
} as unknown as Parameters<typeof entity.of>[0]);
|
|
198
|
+
});
|
|
199
|
+
return entity.toLayer(layer()) as unknown as Layer.Layer<never, never, R>;
|
|
201
200
|
},
|
|
202
201
|
};
|
package/src/index.ts
CHANGED
package/src/inspection.ts
CHANGED
|
@@ -48,6 +48,19 @@ export interface EffectEvent<S> {
|
|
|
48
48
|
readonly timestamp: number;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Event emitted when a transition handler or spawn effect fails with a defect
|
|
53
|
+
*/
|
|
54
|
+
export interface ErrorEvent<S, E> {
|
|
55
|
+
readonly type: "@machine.error";
|
|
56
|
+
readonly actorId: string;
|
|
57
|
+
readonly phase: "transition" | "spawn";
|
|
58
|
+
readonly state: S;
|
|
59
|
+
readonly event: E;
|
|
60
|
+
readonly error: string;
|
|
61
|
+
readonly timestamp: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
51
64
|
/**
|
|
52
65
|
* Event emitted when an actor stops
|
|
53
66
|
*/
|
|
@@ -66,6 +79,7 @@ export type InspectionEvent<S, E> =
|
|
|
66
79
|
| EventReceivedEvent<S, E>
|
|
67
80
|
| TransitionEvent<S, E>
|
|
68
81
|
| EffectEvent<S>
|
|
82
|
+
| ErrorEvent<S, E>
|
|
69
83
|
| StopEvent<S>;
|
|
70
84
|
|
|
71
85
|
// ============================================================================
|
|
@@ -119,6 +133,9 @@ export const consoleInspector = <
|
|
|
119
133
|
case "@machine.effect":
|
|
120
134
|
console.log(prefix, event.effectType, "effect in", event.state._tag);
|
|
121
135
|
break;
|
|
136
|
+
case "@machine.error":
|
|
137
|
+
console.log(prefix, "error in", event.phase, event.state._tag, "-", event.error);
|
|
138
|
+
break;
|
|
122
139
|
case "@machine.stop":
|
|
123
140
|
console.log(prefix, "stopped in", event.finalState._tag);
|
|
124
141
|
break;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Clock, Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { InspectionEvent, Inspector } from "../inspection.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Emit an inspection event with timestamp from Clock.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function* <S, E>(
|
|
10
|
+
inspector: Inspector<S, E> | undefined,
|
|
11
|
+
makeEvent: (timestamp: number) => InspectionEvent<S, E>,
|
|
12
|
+
) {
|
|
13
|
+
if (inspector === undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const timestamp = yield* Clock.currentTimeMillis;
|
|
17
|
+
yield* Effect.try(() => inspector.onInspect(makeEvent(timestamp))).pipe(Effect.ignore);
|
|
18
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @internal
|
|
10
10
|
*/
|
|
11
|
-
import { Effect, Exit, Scope } from "effect";
|
|
11
|
+
import { Cause, Effect, Exit, Scope } from "effect";
|
|
12
12
|
|
|
13
13
|
import type { Machine, MachineRef, Transition, SpawnEffect, HandlerContext } from "../machine.js";
|
|
14
14
|
import type { GuardsDef, EffectsDef, MachineContext } from "../slot.js";
|
|
@@ -40,7 +40,7 @@ export interface TransitionExecutionResult<S> {
|
|
|
40
40
|
*
|
|
41
41
|
* @internal
|
|
42
42
|
*/
|
|
43
|
-
export const runTransitionHandler = <
|
|
43
|
+
export const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(function* <
|
|
44
44
|
S extends { readonly _tag: string },
|
|
45
45
|
E extends { readonly _tag: string },
|
|
46
46
|
R,
|
|
@@ -52,20 +52,19 @@ export const runTransitionHandler = <
|
|
|
52
52
|
state: S,
|
|
53
53
|
event: E,
|
|
54
54
|
self: MachineRef<E>,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const { guards, effects } = machine._createSlotAccessors(ctx);
|
|
55
|
+
) {
|
|
56
|
+
const ctx: MachineContext<S, E, MachineRef<E>> = { state, event, self };
|
|
57
|
+
const { guards, effects } = machine._slots;
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
const handlerCtx: HandlerContext<S, E, GD, EFD> = { state, event, guards, effects };
|
|
60
|
+
const result = transition.handler(handlerCtx);
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
return isEffect(result)
|
|
63
|
+
? yield* (result as Effect.Effect<S, never, R>).pipe(
|
|
64
|
+
Effect.provideService(machine.Context, ctx),
|
|
65
|
+
)
|
|
66
|
+
: result;
|
|
67
|
+
});
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
70
|
* Execute a transition for a given state and event.
|
|
@@ -78,7 +77,7 @@ export const runTransitionHandler = <
|
|
|
78
77
|
*
|
|
79
78
|
* @internal
|
|
80
79
|
*/
|
|
81
|
-
export const executeTransition = <
|
|
80
|
+
export const executeTransition = Effect.fn("effect-machine.executeTransition")(function* <
|
|
82
81
|
S extends { readonly _tag: string },
|
|
83
82
|
E extends { readonly _tag: string },
|
|
84
83
|
R,
|
|
@@ -89,26 +88,25 @@ export const executeTransition = <
|
|
|
89
88
|
currentState: S,
|
|
90
89
|
event: E,
|
|
91
90
|
self: MachineRef<E>,
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
const transition = resolveTransition(machine, currentState, event);
|
|
95
|
-
|
|
96
|
-
if (transition === undefined) {
|
|
97
|
-
return {
|
|
98
|
-
newState: currentState,
|
|
99
|
-
transitioned: false,
|
|
100
|
-
reenter: false,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const newState = yield* runTransitionHandler(machine, transition, currentState, event, self);
|
|
91
|
+
) {
|
|
92
|
+
const transition = resolveTransition(machine, currentState, event);
|
|
105
93
|
|
|
94
|
+
if (transition === undefined) {
|
|
106
95
|
return {
|
|
107
|
-
newState,
|
|
108
|
-
transitioned:
|
|
109
|
-
reenter:
|
|
96
|
+
newState: currentState,
|
|
97
|
+
transitioned: false,
|
|
98
|
+
reenter: false,
|
|
110
99
|
};
|
|
111
|
-
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const newState = yield* runTransitionHandler(machine, transition, currentState, event, self);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
newState,
|
|
106
|
+
transitioned: true,
|
|
107
|
+
reenter: transition.reenter === true,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
112
110
|
|
|
113
111
|
// ============================================================================
|
|
114
112
|
// Event Processing Core (shared by actor and entity-machine)
|
|
@@ -122,6 +120,18 @@ export interface ProcessEventHooks<S, E> {
|
|
|
122
120
|
readonly onSpawnEffect?: (state: S) => Effect.Effect<void>;
|
|
123
121
|
/** Called after transition completes */
|
|
124
122
|
readonly onTransition?: (from: S, to: S, event: E) => Effect.Effect<void>;
|
|
123
|
+
/** Called when a transition handler or spawn effect fails with a defect */
|
|
124
|
+
readonly onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Error info for inspection hooks.
|
|
129
|
+
*/
|
|
130
|
+
export interface ProcessEventError<S, E> {
|
|
131
|
+
readonly phase: "transition" | "spawn";
|
|
132
|
+
readonly state: S;
|
|
133
|
+
readonly event: E;
|
|
134
|
+
readonly cause: Cause.Cause<unknown>;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/**
|
|
@@ -152,7 +162,7 @@ export interface ProcessEventResult<S> {
|
|
|
152
162
|
*
|
|
153
163
|
* @internal
|
|
154
164
|
*/
|
|
155
|
-
export const processEventCore = <
|
|
165
|
+
export const processEventCore = Effect.fn("effect-machine.processEventCore")(function* <
|
|
156
166
|
S extends { readonly _tag: string },
|
|
157
167
|
E extends { readonly _tag: string },
|
|
158
168
|
R,
|
|
@@ -165,62 +175,84 @@ export const processEventCore = <
|
|
|
165
175
|
self: MachineRef<E>,
|
|
166
176
|
stateScopeRef: { current: Scope.CloseableScope },
|
|
167
177
|
hooks?: ProcessEventHooks<S, E>,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
178
|
+
) {
|
|
179
|
+
// Execute transition (defect-aware)
|
|
180
|
+
const result = yield* executeTransition(machine, currentState, event, self).pipe(
|
|
181
|
+
Effect.catchAllCause((cause) => {
|
|
182
|
+
if (Cause.isInterruptedOnly(cause)) {
|
|
183
|
+
return Effect.interrupt;
|
|
184
|
+
}
|
|
185
|
+
const onError = hooks?.onError;
|
|
186
|
+
if (onError === undefined) {
|
|
187
|
+
return Effect.failCause(cause).pipe(Effect.orDie);
|
|
188
|
+
}
|
|
189
|
+
return onError({
|
|
190
|
+
phase: "transition",
|
|
191
|
+
state: currentState,
|
|
192
|
+
event,
|
|
193
|
+
cause,
|
|
194
|
+
}).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
if (!result.transitioned) {
|
|
199
|
+
return {
|
|
200
|
+
newState: currentState,
|
|
201
|
+
previousState: currentState,
|
|
202
|
+
transitioned: false,
|
|
203
|
+
lifecycleRan: false,
|
|
204
|
+
isFinal: false,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
186
207
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
208
|
+
const newState = result.newState;
|
|
209
|
+
const stateTagChanged = newState._tag !== currentState._tag;
|
|
210
|
+
const runLifecycle = stateTagChanged || result.reenter;
|
|
190
211
|
|
|
191
|
-
|
|
192
|
-
|
|
212
|
+
if (runLifecycle) {
|
|
213
|
+
// Close old state scope (interrupts spawn fibers)
|
|
214
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
193
215
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
yield* hooks.onTransition(currentState, newState, event);
|
|
197
|
-
}
|
|
216
|
+
// Create new state scope
|
|
217
|
+
stateScopeRef.current = yield* Scope.make();
|
|
198
218
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
// Hook: transition complete (before spawn effects)
|
|
220
|
+
if (hooks?.onTransition !== undefined) {
|
|
221
|
+
yield* hooks.onTransition(currentState, newState, event);
|
|
222
|
+
}
|
|
203
223
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
yield*
|
|
224
|
+
// Hook: about to run spawn effects
|
|
225
|
+
if (hooks?.onSpawnEffect !== undefined) {
|
|
226
|
+
yield* hooks.onSpawnEffect(newState);
|
|
207
227
|
}
|
|
208
228
|
|
|
209
|
-
|
|
229
|
+
// Run spawn effects for new state
|
|
230
|
+
const enterEvent = { _tag: INTERNAL_ENTER_EVENT } as E;
|
|
231
|
+
yield* runSpawnEffects(
|
|
232
|
+
machine,
|
|
210
233
|
newState,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
234
|
+
enterEvent,
|
|
235
|
+
self,
|
|
236
|
+
stateScopeRef.current,
|
|
237
|
+
hooks?.onError,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
newState,
|
|
243
|
+
previousState: currentState,
|
|
244
|
+
transitioned: true,
|
|
245
|
+
lifecycleRan: runLifecycle,
|
|
246
|
+
isFinal: machine.finalStates.has(newState._tag),
|
|
247
|
+
};
|
|
248
|
+
});
|
|
217
249
|
|
|
218
250
|
/**
|
|
219
251
|
* Run spawn effects for a state (forked into state scope, auto-cancelled on state exit).
|
|
220
252
|
*
|
|
221
253
|
* @internal
|
|
222
254
|
*/
|
|
223
|
-
export const runSpawnEffects = <
|
|
255
|
+
export const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* <
|
|
224
256
|
S extends { readonly _tag: string },
|
|
225
257
|
E extends { readonly _tag: string },
|
|
226
258
|
R,
|
|
@@ -232,25 +264,42 @@ export const runSpawnEffects = <
|
|
|
232
264
|
event: E,
|
|
233
265
|
self: MachineRef<E>,
|
|
234
266
|
stateScope: Scope.CloseableScope,
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
267
|
+
onError?: (info: ProcessEventError<S, E>) => Effect.Effect<void>,
|
|
268
|
+
) {
|
|
269
|
+
const spawnEffects = findSpawnEffects(machine, state._tag);
|
|
270
|
+
const ctx: MachineContext<S, E, MachineRef<E>> = { state, event, self };
|
|
271
|
+
const { effects: effectSlots } = machine._slots;
|
|
272
|
+
const reportError = onError;
|
|
273
|
+
|
|
274
|
+
for (const spawnEffect of spawnEffects) {
|
|
275
|
+
// Fork the spawn effect into the state scope - interrupted when scope closes
|
|
276
|
+
const effect = (
|
|
277
|
+
spawnEffect.handler({ state, event, self, effects: effectSlots }) as Effect.Effect<
|
|
278
|
+
void,
|
|
279
|
+
never,
|
|
280
|
+
R
|
|
281
|
+
>
|
|
282
|
+
).pipe(
|
|
283
|
+
Effect.provideService(machine.Context, ctx),
|
|
284
|
+
Effect.catchAllCause((cause) => {
|
|
285
|
+
if (Cause.isInterruptedOnly(cause)) {
|
|
286
|
+
return Effect.interrupt;
|
|
287
|
+
}
|
|
288
|
+
if (reportError === undefined) {
|
|
289
|
+
return Effect.failCause(cause).pipe(Effect.orDie);
|
|
290
|
+
}
|
|
291
|
+
return reportError({
|
|
292
|
+
phase: "spawn",
|
|
293
|
+
state,
|
|
294
|
+
event,
|
|
295
|
+
cause,
|
|
296
|
+
}).pipe(Effect.zipRight(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
254
303
|
|
|
255
304
|
/**
|
|
256
305
|
* Resolve which transition should fire for a given state and event.
|
|
@@ -300,6 +349,13 @@ interface MachineIndex<S, E, GD extends GuardsDef, EFD extends EffectsDef, R> {
|
|
|
300
349
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
301
350
|
const indexCache = new WeakMap<object, MachineIndex<any, any, any, any, any>>();
|
|
302
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Invalidate cached index for a machine (call after mutation).
|
|
354
|
+
*/
|
|
355
|
+
export const invalidateIndex = (machine: object): void => {
|
|
356
|
+
indexCache.delete(machine);
|
|
357
|
+
};
|
|
358
|
+
|
|
303
359
|
/**
|
|
304
360
|
* Build transition index from machine definition.
|
|
305
361
|
* O(n) where n = number of transitions.
|