effect-machine 0.13.0 → 0.15.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 +19 -11
- package/dist/actor.d.ts +18 -6
- package/dist/actor.js +151 -61
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +2 -1
- package/dist/cluster/to-entity.d.ts +1 -1
- package/dist/errors.d.ts +9 -2
- package/dist/errors.js +8 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/internal/runtime.d.ts +21 -2
- package/dist/internal/runtime.js +64 -56
- package/dist/internal/transition.d.ts +9 -9
- package/dist/internal/transition.js +3 -5
- package/dist/machine.d.ts +129 -135
- package/dist/machine.js +97 -112
- package/dist/schema.d.ts +17 -1
- package/dist/schema.js +10 -0
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/testing.d.ts +16 -16
- package/dist/testing.js +3 -3
- package/package.json +3 -3
- package/v3/dist/actor.d.ts +25 -12
- package/v3/dist/actor.js +174 -89
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/entity-machine.js +1 -0
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +12 -3
- package/v3/dist/errors.js +10 -4
- package/v3/dist/index.d.ts +6 -6
- package/v3/dist/index.js +2 -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 +27 -8
- package/v3/dist/internal/runtime.js +91 -61
- package/v3/dist/internal/transition.d.ts +10 -10
- package/v3/dist/internal/transition.js +8 -10
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +160 -120
- package/v3/dist/machine.js +118 -115
- 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/testing.d.ts +16 -16
- package/v3/dist/testing.js +7 -7
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Brand } from "effect";
|
|
2
2
|
|
|
3
3
|
//#region src/internal/brands.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type StateTypeId = typeof StateTypeId;
|
|
7
|
-
type EventTypeId = typeof EventTypeId;
|
|
4
|
+
type StateTypeId = "effect-machine/StateTypeId";
|
|
5
|
+
type EventTypeId = "effect-machine/EventTypeId";
|
|
8
6
|
interface StateBrand extends Brand.Brand<StateTypeId> {}
|
|
9
7
|
interface EventBrand extends Brand.Brand<EventTypeId> {}
|
|
10
8
|
type BrandedState = {
|
|
@@ -13,8 +11,7 @@ type BrandedState = {
|
|
|
13
11
|
type BrandedEvent = {
|
|
14
12
|
readonly _tag: string;
|
|
15
13
|
} & EventBrand;
|
|
16
|
-
|
|
17
|
-
type SchemaIdTypeId = typeof SchemaIdTypeId;
|
|
14
|
+
type SchemaIdTypeId = "effect-machine/SchemaIdTypeId";
|
|
18
15
|
/**
|
|
19
16
|
* Brand that captures the schema definition type D.
|
|
20
17
|
* Two schemas with identical definition shapes will have compatible brands.
|
|
@@ -33,8 +30,7 @@ type FullEventBrand<D extends Record<string, unknown>> = EventBrand & SchemaIdBr
|
|
|
33
30
|
* Brand that carries the reply type for an event variant.
|
|
34
31
|
* Present only on events defined with Event.reply().
|
|
35
32
|
*/
|
|
36
|
-
|
|
37
|
-
type ReplyTypeId = typeof ReplyTypeId;
|
|
33
|
+
type ReplyTypeId = "effect-machine/ReplyTypeId";
|
|
38
34
|
interface ReplyTypeBrand<R> extends Brand.Brand<ReplyTypeId> {
|
|
39
35
|
readonly _ReplyType: R;
|
|
40
36
|
}
|
|
@@ -14,7 +14,7 @@ const emitWithTimestamp = Effect.fn("effect-machine.emitWithTimestamp")(function
|
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
});
|
|
17
|
-
if (
|
|
17
|
+
if (Effect.isEffect(result)) yield* result.pipe(Effect.catchAllCause(() => Effect.void));
|
|
18
18
|
});
|
|
19
19
|
//#endregion
|
|
20
20
|
export { emitWithTimestamp };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ActorExit } from "../supervision.js";
|
|
2
1
|
import { NoReplyError } from "../errors.js";
|
|
3
|
-
import {
|
|
2
|
+
import { MachineContext, SlotsDef } from "../slot.js";
|
|
3
|
+
import { ActorExit } from "../supervision.js";
|
|
4
4
|
import { ProcessEventHooks, ProcessEventResult } from "./transition.js";
|
|
5
5
|
import { Machine, MachineRef } from "../machine.js";
|
|
6
6
|
import { ActorSystem } from "../actor.js";
|
|
@@ -14,7 +14,7 @@ type RuntimeQueuedEvent<E> = {
|
|
|
14
14
|
} | {
|
|
15
15
|
readonly _tag: "sendWait";
|
|
16
16
|
readonly event: E;
|
|
17
|
-
readonly done: Deferred.Deferred<void>;
|
|
17
|
+
readonly done: Deferred.Deferred<void, unknown>;
|
|
18
18
|
} | {
|
|
19
19
|
readonly _tag: "call";
|
|
20
20
|
readonly event: E;
|
|
@@ -43,18 +43,24 @@ interface RuntimeCellResources<S, E> {
|
|
|
43
43
|
interface RuntimeHandle<S, E> {
|
|
44
44
|
/** Enqueue a fire-and-forget event */
|
|
45
45
|
readonly send: (event: E) => Effect.Effect<void>;
|
|
46
|
-
/** Enqueue event and wait for processing to complete (for RPC Send) */
|
|
47
|
-
readonly sendWait: (event: E) => Effect.Effect<void>;
|
|
46
|
+
/** Enqueue event and wait for processing to complete (for RPC Send). Fails on defect. */
|
|
47
|
+
readonly sendWait: (event: E) => Effect.Effect<void, unknown>;
|
|
48
48
|
/** Enqueue an ask event, returns the reply value */
|
|
49
49
|
readonly ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
|
|
50
50
|
/** Get current state */
|
|
51
51
|
readonly getState: Effect.Effect<S>;
|
|
52
|
-
/** SubscriptionRef for state observation */
|
|
52
|
+
/** SubscriptionRef for state observation (WatchState streaming) */
|
|
53
53
|
readonly stateRef: SubscriptionRef.SubscriptionRef<S>;
|
|
54
54
|
/** Whether the runtime has stopped (final state reached) */
|
|
55
55
|
readonly isStopped: Effect.Effect<boolean>;
|
|
56
56
|
/** Stop the runtime (interrupt event loop, clean up) */
|
|
57
57
|
readonly stop: Effect.Effect<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Start the runtime — fork event loop, background effects, spawn effects.
|
|
60
|
+
* Idempotent: first caller runs initialization, subsequent callers await completion.
|
|
61
|
+
* Events sent before start() are queued and processed when start() runs.
|
|
62
|
+
*/
|
|
63
|
+
readonly start: Effect.Effect<void>;
|
|
58
64
|
/** @internal — raw event queue for direct enqueue (actor.ts uses this for pendingReplies tracking) */
|
|
59
65
|
readonly _queue: Queue.Queue<RuntimeQueuedEvent<E>>;
|
|
60
66
|
/** @internal — stopped ref for direct access */
|
|
@@ -123,7 +129,7 @@ interface ProcessQueuedResult<S> {
|
|
|
123
129
|
* and querying state. The runtime owns:
|
|
124
130
|
* - Event loop fiber
|
|
125
131
|
* - Postpone buffer
|
|
126
|
-
* - Background effects
|
|
132
|
+
* - Background effects (under actorScope)
|
|
127
133
|
* - State scope (spawn effects)
|
|
128
134
|
* - Final state detection
|
|
129
135
|
* - Exit reason via exitDeferred
|
|
@@ -137,6 +143,19 @@ declare const createRuntime: <S extends {
|
|
|
137
143
|
readonly _tag: string;
|
|
138
144
|
}, E extends {
|
|
139
145
|
readonly _tag: string;
|
|
140
|
-
}, R,
|
|
146
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, system: ActorSystem, config: RuntimeConfig<S, E>) => Effect.Effect<{
|
|
147
|
+
stop: Effect.Effect<void, never, never>;
|
|
148
|
+
start: Effect.Effect<void, unknown, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
149
|
+
send: (event: E) => Effect.Effect<void>;
|
|
150
|
+
sendWait: (event: E) => Effect.Effect<void, unknown>;
|
|
151
|
+
ask: (event: E) => Effect.Effect<unknown, NoReplyError>;
|
|
152
|
+
getState: Effect.Effect<S, never, never>;
|
|
153
|
+
stateRef: SubscriptionRef.SubscriptionRef<S>;
|
|
154
|
+
isStopped: Effect.Effect<boolean>;
|
|
155
|
+
_queue: Queue.Queue<RuntimeQueuedEvent<E>>;
|
|
156
|
+
_stoppedRef: Ref.Ref<boolean>;
|
|
157
|
+
exitDeferred: Deferred.Deferred<ActorExit<S>, never>;
|
|
158
|
+
actorScope: Scope.CloseableScope;
|
|
159
|
+
}, never, Scope.Scope>;
|
|
141
160
|
//#endregion
|
|
142
161
|
export { ProcessQueuedResult, RuntimeCellResources, RuntimeConfig, RuntimeHandle, RuntimeLifecycleHooks, RuntimeQueuedEvent, createRuntime };
|
|
@@ -3,7 +3,7 @@ import { NoReplyError } from "../errors.js";
|
|
|
3
3
|
import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
|
|
4
4
|
import { ActorExit } from "../supervision.js";
|
|
5
5
|
import { ActorSystem } from "../actor.js";
|
|
6
|
-
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, SubscriptionRef } from "effect";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Runtime, Schema, Scope, SubscriptionRef } from "effect";
|
|
7
7
|
//#region src/internal/runtime.ts
|
|
8
8
|
/**
|
|
9
9
|
* Shared runtime kernel for machine event processing.
|
|
@@ -28,7 +28,7 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
|
|
|
28
28
|
* and querying state. The runtime owns:
|
|
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
34
|
* - Exit reason via exitDeferred
|
|
@@ -40,11 +40,14 @@ import { Cause, Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope, Subscr
|
|
|
40
40
|
*/
|
|
41
41
|
const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
|
|
42
42
|
const { actorId, hooks, lifecycle } = config;
|
|
43
|
+
const rt = yield* Effect.runtime();
|
|
44
|
+
const fork = Runtime.runFork(rt);
|
|
43
45
|
const stateRef = config.cellResources?.stateRef ?? (yield* SubscriptionRef.make(machine.initial));
|
|
44
46
|
const stoppedRef = config.cellResources?.stoppedRef ?? (yield* Ref.make(false));
|
|
45
47
|
const eventQueue = config.cellResources?.eventQueue ?? (yield* config.queueFactory ?? Queue.unbounded());
|
|
46
48
|
const exitDeferred = yield* Deferred.make();
|
|
47
49
|
const actorScope = yield* Scope.make();
|
|
50
|
+
const deferredReplyRef = { current: void 0 };
|
|
48
51
|
const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
|
|
49
52
|
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
50
53
|
_tag: "send",
|
|
@@ -57,10 +60,18 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
57
60
|
const self = {
|
|
58
61
|
send: selfSend,
|
|
59
62
|
cast: selfSend,
|
|
60
|
-
spawn: onChildSpawned !== void 0 ? (childId, childMachine) => defaultSpawn(childId, childMachine).pipe(Effect.tap((child) => onChildSpawned(childId, child))) : defaultSpawn
|
|
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
|
+
})
|
|
61
73
|
};
|
|
62
74
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
63
|
-
const backgroundFibers = [];
|
|
64
75
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
65
76
|
const ctx = {
|
|
66
77
|
actorId,
|
|
@@ -69,65 +80,81 @@ const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (mach
|
|
|
69
80
|
self,
|
|
70
81
|
system
|
|
71
82
|
};
|
|
72
|
-
const
|
|
73
|
-
for (const bg of machine.backgroundEffects) {
|
|
74
|
-
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
75
|
-
actorId,
|
|
76
|
-
state: machine.initial,
|
|
77
|
-
event: initEvent,
|
|
78
|
-
self,
|
|
79
|
-
effects: effectSlots,
|
|
80
|
-
system
|
|
81
|
-
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
82
|
-
backgroundFibers.push(fiber);
|
|
83
|
-
}
|
|
84
|
-
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
|
|
83
|
+
const slots = machine._slots;
|
|
85
84
|
const loopFiberRef = { current: void 0 };
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
/** Set the exit deferred exactly once. */
|
|
86
|
+
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
87
|
+
const startDeferred = yield* Deferred.make();
|
|
88
|
+
const startedRef = yield* Ref.make(false);
|
|
89
|
+
const start = Effect.gen(function* () {
|
|
90
|
+
if (yield* Ref.getAndSet(startedRef, true)) {
|
|
91
|
+
yield* Deferred.await(startDeferred);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const backgroundFibers = [];
|
|
95
|
+
for (const bg of machine.backgroundEffects) {
|
|
96
|
+
const fiber = yield* bg.handler({
|
|
97
|
+
actorId,
|
|
98
|
+
state: machine.initial,
|
|
99
|
+
event: initEvent,
|
|
100
|
+
self,
|
|
101
|
+
slots,
|
|
102
|
+
system
|
|
103
|
+
}).pipe(Effect.provideService(machine.Context, ctx), Effect.forkIn(actorScope));
|
|
104
|
+
backgroundFibers.push(fiber);
|
|
105
|
+
}
|
|
106
|
+
if (lifecycle?.onInitialSpawnEffects !== void 0) yield* lifecycle.onInitialSpawnEffects(machine.initial);
|
|
107
|
+
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);
|
|
108
|
+
yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError, initialSpawnDefectSignal).pipe(Effect.catchAllCause((cause) => {
|
|
109
|
+
return Effect.gen(function* () {
|
|
110
|
+
yield* Ref.set(stoppedRef, true);
|
|
111
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
112
|
+
yield* Scope.close(actorScope, Exit.void);
|
|
113
|
+
yield* Deferred.succeed(exitDeferred, ActorExit.Defect(cause, "initial-spawn"));
|
|
114
|
+
return yield* Effect.failCause(cause);
|
|
115
|
+
});
|
|
116
|
+
}));
|
|
117
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
118
|
+
if (lifecycle?.onFinal !== void 0) yield* lifecycle.onFinal(machine.initial);
|
|
89
119
|
yield* Ref.set(stoppedRef, true);
|
|
90
120
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
91
121
|
yield* Scope.close(actorScope, Exit.void);
|
|
92
|
-
yield*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
yield*
|
|
101
|
-
|
|
102
|
-
yield*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (backgroundFibers.length > 0) yield* Effect.forkDaemon(Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
|
|
114
|
-
if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.zipRight(Ref.set(stoppedRef, true)), Effect.zipRight(Fiber.interrupt(loopFiber)));
|
|
115
|
-
return Effect.never;
|
|
116
|
-
})))).pipe(Effect.catchAllCause(() => Effect.void)));
|
|
122
|
+
yield* setExit(ActorExit.Final(machine.initial));
|
|
123
|
+
yield* Deferred.succeed(startDeferred, void 0);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const augmentedHooks = {
|
|
127
|
+
...hooks,
|
|
128
|
+
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)
|
|
129
|
+
};
|
|
130
|
+
const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, augmentedHooks, deferredReplyRef, lifecycle, config.wrapProcess, fork));
|
|
131
|
+
loopFiberRef.current = loopFiber;
|
|
132
|
+
if (backgroundFibers.length > 0) yield* Effect.raceAll(backgroundFibers.map((fiber) => Fiber.await(fiber).pipe(Effect.flatMap((exit) => {
|
|
133
|
+
if (exit._tag === "Failure" && !Cause.isInterruptedOnly(exit.cause)) return setExit(ActorExit.Defect(exit.cause, "background")).pipe(Effect.andThen(Ref.set(stoppedRef, true)), Effect.andThen(Fiber.interrupt(loopFiber)));
|
|
134
|
+
return Effect.never;
|
|
135
|
+
})))).pipe(Effect.forkIn(actorScope));
|
|
136
|
+
yield* Effect.forkDaemon(Effect.gen(function* () {
|
|
137
|
+
const loopExit = yield* Fiber.await(loopFiber);
|
|
138
|
+
if (loopExit._tag === "Success") yield* Scope.close(actorScope, Exit.void);
|
|
139
|
+
else yield* Scope.close(actorScope, loopExit);
|
|
140
|
+
}));
|
|
141
|
+
yield* Deferred.succeed(startDeferred, void 0);
|
|
142
|
+
}).pipe(Effect.catchAllCause((cause) => Deferred.failCause(startDeferred, cause).pipe(Effect.andThen(Effect.failCause(cause)))));
|
|
117
143
|
const stop = Effect.gen(function* () {
|
|
118
144
|
if (yield* Ref.get(stoppedRef)) return;
|
|
119
145
|
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
120
146
|
yield* Ref.set(stoppedRef, true);
|
|
121
|
-
|
|
147
|
+
const loopFiber = loopFiberRef.current;
|
|
148
|
+
if (loopFiber !== void 0) yield* Fiber.interrupt(loopFiber);
|
|
122
149
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
123
150
|
yield* Scope.close(actorScope, Exit.void);
|
|
124
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
125
151
|
yield* setExit(ActorExit.Stopped);
|
|
126
152
|
}).pipe(Effect.asVoid);
|
|
127
153
|
if (config.skipFinalizer !== true) yield* Effect.addFinalizer(() => stop);
|
|
128
154
|
return {
|
|
129
155
|
...makeHandle(stateRef, stoppedRef, eventQueue, exitDeferred, actorScope),
|
|
130
|
-
stop
|
|
156
|
+
stop,
|
|
157
|
+
start
|
|
131
158
|
};
|
|
132
159
|
});
|
|
133
160
|
/**
|
|
@@ -169,12 +196,14 @@ const makeHandle = (stateRef, stoppedRef, eventQueue, exitDeferred, actorScope)
|
|
|
169
196
|
stateRef,
|
|
170
197
|
isStopped: Ref.get(stoppedRef),
|
|
171
198
|
stop: Effect.void,
|
|
199
|
+
start: Effect.void,
|
|
172
200
|
_queue: eventQueue,
|
|
173
201
|
_stoppedRef: stoppedRef,
|
|
174
202
|
exitDeferred,
|
|
175
203
|
actorScope
|
|
176
204
|
});
|
|
177
|
-
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, lifecycle, wrapProcess) {
|
|
205
|
+
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, stateScopeRef, actorId, system, exitDeferred, hooks, deferredReplyRef, lifecycle, wrapProcess, fork) {
|
|
206
|
+
const forkEffect = fork ?? Effect.runFork;
|
|
178
207
|
/** Set the exit deferred exactly once. */
|
|
179
208
|
const setExit = (exit) => Deferred.succeed(exitDeferred, exit).pipe(Effect.asVoid);
|
|
180
209
|
const postponed = [];
|
|
@@ -242,7 +271,8 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
242
271
|
}
|
|
243
272
|
yield* Deferred.succeed(queued.reply, decoded);
|
|
244
273
|
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
245
|
-
} else
|
|
274
|
+
} else if (result.deferReply && deferredReplyRef !== void 0) deferredReplyRef.current = queued.reply;
|
|
275
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
246
276
|
actorId,
|
|
247
277
|
eventTag: event._tag
|
|
248
278
|
}));
|
|
@@ -260,16 +290,16 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
260
290
|
const shutdown = (exitReason) => Effect.gen(function* () {
|
|
261
291
|
yield* Ref.set(stoppedRef, true);
|
|
262
292
|
if (lifecycle?.onShutdown !== void 0) yield* lifecycle.onShutdown();
|
|
263
|
-
settlePostponed(postponed, actorId);
|
|
293
|
+
settlePostponed(postponed, actorId, forkEffect);
|
|
264
294
|
const remaining = yield* Queue.takeAll(eventQueue);
|
|
265
|
-
for (const entry of remaining) if (entry._tag === "sendWait")
|
|
266
|
-
else if (entry._tag === "ask")
|
|
295
|
+
for (const entry of remaining) if (entry._tag === "sendWait") forkEffect(Deferred.succeed(entry.done, void 0));
|
|
296
|
+
else if (entry._tag === "ask") forkEffect(Deferred.fail(entry.reply, new NoReplyError({
|
|
267
297
|
actorId,
|
|
268
298
|
eventTag: entry.event._tag
|
|
269
299
|
})));
|
|
270
300
|
else if (entry._tag === "call") {
|
|
271
301
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
272
|
-
|
|
302
|
+
forkEffect(Deferred.succeed(entry.reply, {
|
|
273
303
|
newState: currentState,
|
|
274
304
|
previousState: currentState,
|
|
275
305
|
transitioned: false,
|
|
@@ -296,10 +326,10 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
296
326
|
const { shouldStop, stateChanged } = yield* (wrapProcess !== void 0 ? Effect.gen(function* () {
|
|
297
327
|
return yield* wrapProcess(yield* SubscriptionRef.get(stateRef), eventQueued.event, processInner);
|
|
298
328
|
}) : processInner).pipe(Effect.catchAllCause((cause) => {
|
|
299
|
-
if (queued._tag === "sendWait")
|
|
300
|
-
else if (queued._tag === "ask")
|
|
301
|
-
else if (queued._tag === "call")
|
|
302
|
-
return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.
|
|
329
|
+
if (queued._tag === "sendWait") forkEffect(Deferred.failCause(queued.done, cause));
|
|
330
|
+
else if (queued._tag === "ask") forkEffect(Deferred.die(queued.reply, cause));
|
|
331
|
+
else if (queued._tag === "call") forkEffect(Deferred.failCause(queued.reply, cause));
|
|
332
|
+
return shutdown(ActorExit.Defect(cause, "transition")).pipe(Effect.andThen(Effect.failCause(cause)));
|
|
303
333
|
}));
|
|
304
334
|
if (shouldStop) {
|
|
305
335
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
@@ -323,12 +353,12 @@ const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function*
|
|
|
323
353
|
}
|
|
324
354
|
});
|
|
325
355
|
/** Settle all pending Deferreds in the postpone buffer on shutdown. */
|
|
326
|
-
const settlePostponed = (postponed, actorId) => {
|
|
327
|
-
for (const entry of postponed) if (entry._tag === "ask")
|
|
356
|
+
const settlePostponed = (postponed, actorId, forkFn) => {
|
|
357
|
+
for (const entry of postponed) if (entry._tag === "ask") forkFn(Deferred.fail(entry.reply, new NoReplyError({
|
|
328
358
|
actorId,
|
|
329
359
|
eventTag: entry.event._tag
|
|
330
360
|
})));
|
|
331
|
-
else if (entry._tag === "sendWait")
|
|
361
|
+
else if (entry._tag === "sendWait") forkFn(Deferred.succeed(entry.done, void 0));
|
|
332
362
|
postponed.length = 0;
|
|
333
363
|
};
|
|
334
364
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MachineContext, SlotsDef } from "../slot.js";
|
|
2
2
|
import { Machine, MachineRef, SpawnEffect, Transition } from "../machine.js";
|
|
3
3
|
import { ActorSystem } from "../actor.js";
|
|
4
4
|
import { Cause, Effect, Scope } from "effect";
|
|
@@ -21,7 +21,7 @@ interface TransitionExecutionResult<S> {
|
|
|
21
21
|
*
|
|
22
22
|
* Used by:
|
|
23
23
|
* - executeTransition (actor event loop, testing)
|
|
24
|
-
* -
|
|
24
|
+
* - Machine.replay (event sourcing restore)
|
|
25
25
|
*
|
|
26
26
|
* @internal
|
|
27
27
|
*/
|
|
@@ -29,7 +29,7 @@ declare const runTransitionHandler: <S extends {
|
|
|
29
29
|
readonly _tag: string;
|
|
30
30
|
}, E extends {
|
|
31
31
|
readonly _tag: string;
|
|
32
|
-
}, R,
|
|
32
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, transition: Transition<S, E, SD, R>, state: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
|
|
33
33
|
newState: S;
|
|
34
34
|
hasReply: boolean;
|
|
35
35
|
deferReply: boolean;
|
|
@@ -50,7 +50,7 @@ declare const executeTransition: <S extends {
|
|
|
50
50
|
readonly _tag: string;
|
|
51
51
|
}, E extends {
|
|
52
52
|
readonly _tag: string;
|
|
53
|
-
}, R,
|
|
53
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, system: ActorSystem, actorId: string) => Effect.Effect<{
|
|
54
54
|
newState: S;
|
|
55
55
|
transitioned: boolean;
|
|
56
56
|
reenter: boolean;
|
|
@@ -111,7 +111,7 @@ declare const shouldPostpone: <S extends {
|
|
|
111
111
|
readonly _tag: string;
|
|
112
112
|
}, E extends {
|
|
113
113
|
readonly _tag: string;
|
|
114
|
-
}, R>(machine: Machine<S, E, R, any, any, any
|
|
114
|
+
}, R>(machine: Machine<S, E, R, any, any, any>, stateTag: string, eventTag: string) => boolean;
|
|
115
115
|
/**
|
|
116
116
|
* Process a single event through the machine.
|
|
117
117
|
*
|
|
@@ -128,7 +128,7 @@ declare const processEventCore: <S extends {
|
|
|
128
128
|
readonly _tag: string;
|
|
129
129
|
}, E extends {
|
|
130
130
|
readonly _tag: string;
|
|
131
|
-
}, R,
|
|
131
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, currentState: S, event: E, self: MachineRef<E>, stateScopeRef: {
|
|
132
132
|
current: Scope.CloseableScope;
|
|
133
133
|
}, system: ActorSystem, actorId: string, hooks?: ProcessEventHooks<S, E> | undefined) => Effect.Effect<{
|
|
134
134
|
newState: S;
|
|
@@ -150,7 +150,7 @@ declare const runSpawnEffects: <S extends {
|
|
|
150
150
|
readonly _tag: string;
|
|
151
151
|
}, E extends {
|
|
152
152
|
readonly _tag: string;
|
|
153
|
-
}, R,
|
|
153
|
+
}, R, SD extends SlotsDef>(machine: Machine<S, E, R, any, any, SD>, state: S, event: E, self: MachineRef<E>, stateScope: Scope.CloseableScope, system: ActorSystem, actorId: string, onError?: ((info: ProcessEventError<S, E>) => Effect.Effect<void>) | undefined, onSpawnDefect?: ((cause: Cause.Cause<unknown>) => Effect.Effect<void>) | undefined) => Effect.Effect<void, never, Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
154
154
|
/**
|
|
155
155
|
* Resolve which transition should fire for a given state and event.
|
|
156
156
|
* Uses indexed O(1) lookup. First matching transition wins.
|
|
@@ -159,7 +159,7 @@ declare const resolveTransition: <S extends {
|
|
|
159
159
|
readonly _tag: string;
|
|
160
160
|
}, E extends {
|
|
161
161
|
readonly _tag: string;
|
|
162
|
-
}, 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;
|
|
163
163
|
/**
|
|
164
164
|
* Invalidate cached index for a machine (call after mutation).
|
|
165
165
|
*/
|
|
@@ -174,7 +174,7 @@ declare const findTransitions: <S extends {
|
|
|
174
174
|
readonly _tag: string;
|
|
175
175
|
}, E extends {
|
|
176
176
|
readonly _tag: string;
|
|
177
|
-
}, 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>>;
|
|
178
178
|
/**
|
|
179
179
|
* Find all spawn effects for a state.
|
|
180
180
|
* Returns empty array if no matches.
|
|
@@ -185,6 +185,6 @@ declare const findSpawnEffects: <S extends {
|
|
|
185
185
|
readonly _tag: string;
|
|
186
186
|
}, E extends {
|
|
187
187
|
readonly _tag: string;
|
|
188
|
-
}, 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>>;
|
|
189
189
|
//#endregion
|
|
190
190
|
export { ProcessEventError, ProcessEventHooks, ProcessEventResult, TransitionExecutionResult, executeTransition, findSpawnEffects, findTransitions, invalidateIndex, processEventCore, resolveTransition, runSpawnEffects, runTransitionHandler, shouldPostpone };
|
|
@@ -17,7 +17,7 @@ import { Cause, Effect, Exit, Scope } from "effect";
|
|
|
17
17
|
*
|
|
18
18
|
* Used by:
|
|
19
19
|
* - executeTransition (actor event loop, testing)
|
|
20
|
-
* -
|
|
20
|
+
* - Machine.replay (event sourcing restore)
|
|
21
21
|
*
|
|
22
22
|
* @internal
|
|
23
23
|
*/
|
|
@@ -29,12 +29,10 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
|
|
|
29
29
|
self,
|
|
30
30
|
system
|
|
31
31
|
};
|
|
32
|
-
const { guards, effects } = machine._slots;
|
|
33
32
|
const handlerCtx = {
|
|
34
33
|
state,
|
|
35
34
|
event,
|
|
36
|
-
|
|
37
|
-
effects
|
|
35
|
+
slots: machine._slots
|
|
38
36
|
};
|
|
39
37
|
const raw = transition.handler(handlerCtx);
|
|
40
38
|
const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
|
|
@@ -118,7 +116,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
118
116
|
state: currentState,
|
|
119
117
|
event,
|
|
120
118
|
cause
|
|
121
|
-
}).pipe(Effect.
|
|
119
|
+
}).pipe(Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
122
120
|
}));
|
|
123
121
|
if (!result.transitioned) return {
|
|
124
122
|
newState: currentState,
|
|
@@ -166,7 +164,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
166
164
|
self,
|
|
167
165
|
system
|
|
168
166
|
};
|
|
169
|
-
const
|
|
167
|
+
const slots = machine._slots;
|
|
170
168
|
const reportError = onError;
|
|
171
169
|
const defectSignal = onSpawnDefect;
|
|
172
170
|
for (const spawnEffect of spawnEffects) {
|
|
@@ -175,7 +173,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
175
173
|
state,
|
|
176
174
|
event,
|
|
177
175
|
self,
|
|
178
|
-
|
|
176
|
+
slots,
|
|
179
177
|
system
|
|
180
178
|
}).pipe(Effect.provideService(machine.Context, ctx), Effect.catchAllCause((cause) => {
|
|
181
179
|
if (Cause.isInterruptedOnly(cause)) return Effect.interrupt;
|
|
@@ -186,7 +184,7 @@ const runSpawnEffects = Effect.fn("effect-machine.runSpawnEffects")(function* (m
|
|
|
186
184
|
cause
|
|
187
185
|
}) : Effect.void;
|
|
188
186
|
const signal = defectSignal !== void 0 ? defectSignal(cause) : Effect.void;
|
|
189
|
-
return report.pipe(Effect.
|
|
187
|
+
return report.pipe(Effect.andThen(signal), Effect.andThen(Effect.failCause(cause).pipe(Effect.orDie)));
|
|
190
188
|
}));
|
|
191
189
|
yield* Effect.forkScoped(effect).pipe(Effect.provideService(Scope.Scope, stateScope));
|
|
192
190
|
}
|
|
@@ -261,8 +259,8 @@ const getIndex = (machine) => {
|
|
|
261
259
|
*
|
|
262
260
|
* O(1) lookup after first access (index is lazily built).
|
|
263
261
|
*/
|
|
264
|
-
const findTransitions = (
|
|
265
|
-
const index = getIndex(
|
|
262
|
+
const findTransitions = (machine, stateTag, eventTag) => {
|
|
263
|
+
const index = getIndex(machine);
|
|
266
264
|
const specific = index.transitions.get(stateTag)?.get(eventTag) ?? [];
|
|
267
265
|
if (specific.length > 0) return specific;
|
|
268
266
|
return index.transitions.get("*")?.get(eventTag) ?? [];
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
2
|
//#region src/internal/utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Internal utilities for effect-machine.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
3
7
|
const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
|
|
4
8
|
/**
|
|
5
9
|
* Create a reply result for ask-bearing event handlers.
|
|
@@ -50,7 +54,7 @@ const getTag = (constructorOrValue) => {
|
|
|
50
54
|
}
|
|
51
55
|
};
|
|
52
56
|
/** Check if a value is an Effect */
|
|
53
|
-
const isEffect =
|
|
57
|
+
const isEffect = Effect.isEffect;
|
|
54
58
|
/**
|
|
55
59
|
* Stub ActorSystem that dies on any method call.
|
|
56
60
|
* Used in contexts where spawning/system access isn't supported
|