effect-machine 0.11.0 → 0.12.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/dist/actor.d.ts +10 -4
- package/dist/actor.js +33 -5
- 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 +167 -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 +12 -1
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +3 -2
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +67 -0
- package/dist/internal/runtime.js +248 -0
- package/dist/internal/transition.d.ts +5 -0
- package/dist/internal/transition.js +15 -3
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +26 -13
- package/dist/machine.js +14 -3
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +32 -3
- package/dist/testing.js +4 -2
- package/package.json +3 -3
- package/v3/dist/actor.d.ts +4 -3
- package/v3/dist/actor.js +15 -3
- 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 +16 -1
- package/v3/dist/errors.js +8 -1
- package/v3/dist/index.d.ts +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +65 -0
- package/v3/dist/internal/runtime.js +236 -0
- package/v3/dist/internal/transition.d.ts +5 -0
- package/v3/dist/internal/transition.js +15 -3
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +19 -13
- package/v3/dist/machine.js +10 -2
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { INTERNAL_INIT_EVENT } from "./utils.js";
|
|
2
|
+
import { NoReplyError } from "../errors.js";
|
|
3
|
+
import { processEventCore, runSpawnEffects, shouldPostpone } from "./transition.js";
|
|
4
|
+
import { ActorSystem } from "../actor.js";
|
|
5
|
+
import { Deferred, Effect, Exit, Fiber, Queue, Ref, Schema, Scope } from "effect";
|
|
6
|
+
//#region src/internal/runtime.ts
|
|
7
|
+
/**
|
|
8
|
+
* Shared runtime kernel for machine event processing.
|
|
9
|
+
*
|
|
10
|
+
* Provides a single-queue event loop with:
|
|
11
|
+
* - Sequential event processing (no split-mailbox race)
|
|
12
|
+
* - Postpone buffer with drain-on-state-change (gen_statem)
|
|
13
|
+
* - Background effect lifecycle
|
|
14
|
+
* - Spawn effect lifecycle (per-state scope)
|
|
15
|
+
* - Final state detection → stop
|
|
16
|
+
* - Reply settlement (call/ask Deferreds)
|
|
17
|
+
* - Reply schema validation
|
|
18
|
+
*
|
|
19
|
+
* Used by entity-machine. Local actor (actor.ts) has its own event loop
|
|
20
|
+
* with additional concerns (inspection, listeners, subscription ref, etc.)
|
|
21
|
+
* that will be migrated to use this kernel in a future refactor.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Create a runtime for a machine. Returns a handle for sending events
|
|
27
|
+
* and querying state. The runtime owns:
|
|
28
|
+
* - Single event queue (all events serialized)
|
|
29
|
+
* - Event loop fiber
|
|
30
|
+
* - Postpone buffer
|
|
31
|
+
* - Background effects
|
|
32
|
+
* - State scope (spawn effects)
|
|
33
|
+
* - Final state detection
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
const createRuntime = Effect.fn("effect-machine.runtime.create")(function* (machine, system, config) {
|
|
38
|
+
const { actorId, hooks } = config;
|
|
39
|
+
const stateRef = yield* Ref.make(machine.initial);
|
|
40
|
+
const stoppedRef = yield* Ref.make(false);
|
|
41
|
+
const eventQueue = yield* config.queueFactory ?? Queue.unbounded();
|
|
42
|
+
const selfSend = Effect.fn("effect-machine.runtime.self.send")(function* (event) {
|
|
43
|
+
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
44
|
+
_tag: "send",
|
|
45
|
+
event
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const self = {
|
|
49
|
+
send: selfSend,
|
|
50
|
+
cast: selfSend,
|
|
51
|
+
spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
|
|
52
|
+
};
|
|
53
|
+
const stateScopeRef = { current: yield* Scope.make() };
|
|
54
|
+
const backgroundFibers = [];
|
|
55
|
+
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
56
|
+
const ctx = {
|
|
57
|
+
actorId,
|
|
58
|
+
state: machine.initial,
|
|
59
|
+
event: initEvent,
|
|
60
|
+
self,
|
|
61
|
+
system
|
|
62
|
+
};
|
|
63
|
+
const { effects: effectSlots } = machine._slots;
|
|
64
|
+
for (const bg of machine.backgroundEffects) {
|
|
65
|
+
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
66
|
+
actorId,
|
|
67
|
+
state: machine.initial,
|
|
68
|
+
event: initEvent,
|
|
69
|
+
self,
|
|
70
|
+
effects: effectSlots,
|
|
71
|
+
system
|
|
72
|
+
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
73
|
+
backgroundFibers.push(fiber);
|
|
74
|
+
}
|
|
75
|
+
yield* runSpawnEffects(machine, machine.initial, initEvent, self, stateScopeRef.current, system, actorId, hooks?.onError);
|
|
76
|
+
if (machine.finalStates.has(machine.initial._tag)) {
|
|
77
|
+
yield* Ref.set(stoppedRef, true);
|
|
78
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
79
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
80
|
+
return makeHandle(stateRef, stoppedRef, eventQueue, machine);
|
|
81
|
+
}
|
|
82
|
+
const loopFiber = yield* Effect.forkDaemon(runtimeEventLoop(machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks));
|
|
83
|
+
const stop = Effect.gen(function* () {
|
|
84
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
85
|
+
yield* Ref.set(stoppedRef, true);
|
|
86
|
+
yield* Fiber.interrupt(loopFiber);
|
|
87
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
88
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
89
|
+
}).pipe(Effect.asVoid);
|
|
90
|
+
yield* Effect.addFinalizer(() => stop);
|
|
91
|
+
return {
|
|
92
|
+
...makeHandle(stateRef, stoppedRef, eventQueue, machine),
|
|
93
|
+
stop
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Build the runtime handle (send/ask/getState/isStopped).
|
|
98
|
+
* Shared between initial-final and normal paths.
|
|
99
|
+
*/
|
|
100
|
+
const makeHandle = (stateRef, stoppedRef, eventQueue, _machine) => ({
|
|
101
|
+
send: (event) => Effect.gen(function* () {
|
|
102
|
+
if (!(yield* Ref.get(stoppedRef))) yield* Queue.offer(eventQueue, {
|
|
103
|
+
_tag: "send",
|
|
104
|
+
event
|
|
105
|
+
});
|
|
106
|
+
}),
|
|
107
|
+
sendWait: (event) => Effect.gen(function* () {
|
|
108
|
+
if (!(yield* Ref.get(stoppedRef))) {
|
|
109
|
+
const done = yield* Deferred.make();
|
|
110
|
+
yield* Queue.offer(eventQueue, {
|
|
111
|
+
_tag: "sendWait",
|
|
112
|
+
event,
|
|
113
|
+
done
|
|
114
|
+
});
|
|
115
|
+
yield* Deferred.await(done);
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
ask: (event) => Effect.gen(function* () {
|
|
119
|
+
if (yield* Ref.get(stoppedRef)) return yield* new NoReplyError({
|
|
120
|
+
actorId: "stopped",
|
|
121
|
+
eventTag: event._tag
|
|
122
|
+
});
|
|
123
|
+
const reply = yield* Deferred.make();
|
|
124
|
+
yield* Queue.offer(eventQueue, {
|
|
125
|
+
_tag: "ask",
|
|
126
|
+
event,
|
|
127
|
+
reply
|
|
128
|
+
});
|
|
129
|
+
return yield* Deferred.await(reply);
|
|
130
|
+
}),
|
|
131
|
+
getState: Ref.get(stateRef),
|
|
132
|
+
isStopped: Ref.get(stoppedRef),
|
|
133
|
+
stop: Effect.void
|
|
134
|
+
});
|
|
135
|
+
const runtimeEventLoop = Effect.fn("effect-machine.runtime.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, backgroundFibers, stateScopeRef, actorId, system, hooks) {
|
|
136
|
+
const postponed = [];
|
|
137
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
138
|
+
const processQueued = Effect.fn("effect-machine.runtime.processQueued")(function* (queued) {
|
|
139
|
+
const event = queued.event;
|
|
140
|
+
const currentState = yield* Ref.get(stateRef);
|
|
141
|
+
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
142
|
+
postponed.push({
|
|
143
|
+
_tag: "send",
|
|
144
|
+
event
|
|
145
|
+
});
|
|
146
|
+
if (queued._tag === "sendWait") yield* Deferred.succeed(queued.done, void 0);
|
|
147
|
+
return {
|
|
148
|
+
shouldStop: false,
|
|
149
|
+
stateChanged: false
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, hooks);
|
|
153
|
+
if (result.transitioned) yield* Ref.set(stateRef, result.newState);
|
|
154
|
+
switch (queued._tag) {
|
|
155
|
+
case "sendWait":
|
|
156
|
+
yield* Deferred.succeed(queued.done, void 0);
|
|
157
|
+
break;
|
|
158
|
+
case "ask":
|
|
159
|
+
if (result.hasReply) {
|
|
160
|
+
const replySchema = machine._replySchemas?.get(event._tag);
|
|
161
|
+
if (replySchema !== void 0) {
|
|
162
|
+
let decoded;
|
|
163
|
+
try {
|
|
164
|
+
decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
|
|
165
|
+
} catch (decodeError) {
|
|
166
|
+
yield* Deferred.die(queued.reply, decodeError);
|
|
167
|
+
return yield* Effect.die(decodeError);
|
|
168
|
+
}
|
|
169
|
+
yield* Deferred.succeed(queued.reply, decoded);
|
|
170
|
+
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
171
|
+
} else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
172
|
+
actorId,
|
|
173
|
+
eventTag: event._tag
|
|
174
|
+
}));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
shouldStop: result.isFinal && result.lifecycleRan,
|
|
179
|
+
stateChanged: result.lifecycleRan
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
while (true) {
|
|
183
|
+
const queued = yield* Queue.take(eventQueue);
|
|
184
|
+
const { shouldStop, stateChanged } = yield* processQueued(queued).pipe(Effect.catchAllCause((cause) => {
|
|
185
|
+
if (queued._tag === "sendWait") Effect.runFork(Deferred.succeed(queued.done, void 0));
|
|
186
|
+
else if (queued._tag === "ask") Effect.runFork(Deferred.die(queued.reply, cause));
|
|
187
|
+
return Effect.failCause(cause);
|
|
188
|
+
}));
|
|
189
|
+
if (shouldStop) {
|
|
190
|
+
yield* Ref.set(stoppedRef, true);
|
|
191
|
+
settlePostponed(postponed, actorId);
|
|
192
|
+
const remaining = yield* Queue.takeAll(eventQueue);
|
|
193
|
+
for (const entry of remaining) if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
|
|
194
|
+
else if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
|
|
195
|
+
actorId,
|
|
196
|
+
eventTag: entry.event._tag
|
|
197
|
+
})));
|
|
198
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
199
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let drainTriggered = stateChanged;
|
|
203
|
+
while (drainTriggered && postponed.length > 0) {
|
|
204
|
+
drainTriggered = false;
|
|
205
|
+
const drained = postponed.splice(0);
|
|
206
|
+
for (const entry of drained) {
|
|
207
|
+
const drain = yield* processQueued(entry);
|
|
208
|
+
if (drain.shouldStop) {
|
|
209
|
+
yield* Ref.set(stoppedRef, true);
|
|
210
|
+
settlePostponed(postponed, actorId);
|
|
211
|
+
const remaining2 = yield* Queue.takeAll(eventQueue);
|
|
212
|
+
for (const r of remaining2) if (r._tag === "sendWait") Effect.runFork(Deferred.succeed(r.done, void 0));
|
|
213
|
+
else if (r._tag === "ask") Effect.runFork(Deferred.fail(r.reply, new NoReplyError({
|
|
214
|
+
actorId,
|
|
215
|
+
eventTag: r.event._tag
|
|
216
|
+
})));
|
|
217
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
218
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (drain.stateChanged) drainTriggered = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
/** Settle all pending Deferreds in the postpone buffer on shutdown. */
|
|
227
|
+
const settlePostponed = (postponed, actorId) => {
|
|
228
|
+
for (const entry of postponed) if (entry._tag === "ask") Effect.runFork(Deferred.fail(entry.reply, new NoReplyError({
|
|
229
|
+
actorId,
|
|
230
|
+
eventTag: entry.event._tag
|
|
231
|
+
})));
|
|
232
|
+
else if (entry._tag === "sendWait") Effect.runFork(Deferred.succeed(entry.done, void 0));
|
|
233
|
+
postponed.length = 0;
|
|
234
|
+
};
|
|
235
|
+
//#endregion
|
|
236
|
+
export { createRuntime };
|
|
@@ -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
|
/**
|
|
@@ -92,6 +94,8 @@ interface ProcessEventResult<S> {
|
|
|
92
94
|
readonly isFinal: boolean;
|
|
93
95
|
/** Whether the handler provided a reply (structural, not value-based) */
|
|
94
96
|
readonly hasReply: boolean;
|
|
97
|
+
/** Whether the handler deferred the reply to a spawn handler (Machine.deferReply) */
|
|
98
|
+
readonly deferReply: boolean;
|
|
95
99
|
/** Domain reply value from handler (used by ask). Only meaningful when hasReply is true. */
|
|
96
100
|
readonly reply?: unknown;
|
|
97
101
|
/** Whether the event was postponed (buffered for retry after next state change) */
|
|
@@ -131,6 +135,7 @@ declare const processEventCore: <S extends {
|
|
|
131
135
|
lifecycleRan: boolean;
|
|
132
136
|
isFinal: boolean;
|
|
133
137
|
hasReply: boolean;
|
|
138
|
+
deferReply: boolean;
|
|
134
139
|
reply: unknown;
|
|
135
140
|
postponed: boolean;
|
|
136
141
|
}, never, Exclude<R, MachineContext<S, E, MachineRef<E>>> | Exclude<Exclude<R, MachineContext<S, E, MachineRef<E>>>, Scope.Scope>>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { INTERNAL_ENTER_EVENT, isEffect } from "./utils.js";
|
|
1
|
+
import { INTERNAL_ENTER_EVENT, isDeferReplyResult, isEffect, isReplyResult } from "./utils.js";
|
|
2
2
|
import { BuiltMachine } from "../machine.js";
|
|
3
3
|
import { Cause, Effect, Exit, Scope } from "effect";
|
|
4
4
|
//#region src/internal/transition.ts
|
|
@@ -39,14 +39,22 @@ const runTransitionHandler = Effect.fn("effect-machine.runTransitionHandler")(fu
|
|
|
39
39
|
};
|
|
40
40
|
const raw = transition.handler(handlerCtx);
|
|
41
41
|
const resolved = isEffect(raw) ? yield* raw.pipe(Effect.provideService(machine.Context, ctx)) : raw;
|
|
42
|
-
if (
|
|
42
|
+
if (isReplyResult(resolved)) return {
|
|
43
43
|
newState: resolved.state,
|
|
44
44
|
hasReply: true,
|
|
45
|
+
deferReply: false,
|
|
45
46
|
reply: resolved.reply
|
|
46
47
|
};
|
|
48
|
+
if (isDeferReplyResult(resolved)) return {
|
|
49
|
+
newState: resolved.state,
|
|
50
|
+
hasReply: false,
|
|
51
|
+
deferReply: true,
|
|
52
|
+
reply: void 0
|
|
53
|
+
};
|
|
47
54
|
return {
|
|
48
55
|
newState: resolved,
|
|
49
56
|
hasReply: false,
|
|
57
|
+
deferReply: false,
|
|
50
58
|
reply: void 0
|
|
51
59
|
};
|
|
52
60
|
});
|
|
@@ -68,14 +76,16 @@ const executeTransition = Effect.fn("effect-machine.executeTransition")(function
|
|
|
68
76
|
transitioned: false,
|
|
69
77
|
reenter: false,
|
|
70
78
|
hasReply: false,
|
|
79
|
+
deferReply: false,
|
|
71
80
|
reply: void 0
|
|
72
81
|
};
|
|
73
|
-
const { newState, hasReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
|
|
82
|
+
const { newState, hasReply, deferReply, reply } = yield* runTransitionHandler(machine, transition, currentState, event, self, system, actorId);
|
|
74
83
|
return {
|
|
75
84
|
newState,
|
|
76
85
|
transitioned: true,
|
|
77
86
|
reenter: transition.reenter === true,
|
|
78
87
|
hasReply,
|
|
88
|
+
deferReply,
|
|
79
89
|
reply
|
|
80
90
|
};
|
|
81
91
|
});
|
|
@@ -118,6 +128,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
118
128
|
lifecycleRan: false,
|
|
119
129
|
isFinal: false,
|
|
120
130
|
hasReply: false,
|
|
131
|
+
deferReply: false,
|
|
121
132
|
reply: void 0,
|
|
122
133
|
postponed: false
|
|
123
134
|
};
|
|
@@ -137,6 +148,7 @@ const processEventCore = Effect.fn("effect-machine.processEventCore")(function*
|
|
|
137
148
|
lifecycleRan: runLifecycle,
|
|
138
149
|
isFinal: machine.finalStates.has(newState._tag),
|
|
139
150
|
hasReply: result.hasReply,
|
|
151
|
+
deferReply: result.deferReply,
|
|
140
152
|
reply: result.reply,
|
|
141
153
|
postponed: false
|
|
142
154
|
};
|
|
@@ -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
|
-
|
|
27
|
-
|
|
26
|
+
declare const ReplyResultSymbol: unique symbol;
|
|
27
|
+
type ReplyResultSymbol = typeof ReplyResultSymbol;
|
|
28
|
+
/**
|
|
29
|
+
* Branded reply result from a transition handler.
|
|
30
|
+
* Created via `Machine.reply(state, value)`.
|
|
31
|
+
*/
|
|
32
|
+
interface ReplyResult<State, Reply> {
|
|
33
|
+
readonly state: State;
|
|
34
|
+
readonly reply: Reply;
|
|
35
|
+
readonly [ReplyResultSymbol]: true;
|
|
36
|
+
}
|
|
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> {
|
|
28
53
|
readonly state: State;
|
|
29
|
-
readonly
|
|
54
|
+
readonly [DeferReplySymbol]: true;
|
|
30
55
|
}
|
|
31
56
|
/**
|
|
32
|
-
*
|
|
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()
|
|
33
69
|
*/
|
|
34
|
-
type TransitionResult<State, R> = State |
|
|
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 };
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { Effect, Stream } from "effect";
|
|
2
2
|
//#region src/internal/utils.ts
|
|
3
|
+
const ReplyResultSymbol = Symbol.for("effect-machine/ReplyResult");
|
|
4
|
+
/**
|
|
5
|
+
* Create a reply result for ask-bearing event handlers.
|
|
6
|
+
*/
|
|
7
|
+
const makeReply = (state, reply) => ({
|
|
8
|
+
state,
|
|
9
|
+
reply,
|
|
10
|
+
[ReplyResultSymbol]: true
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Type guard for ReplyResult (symbol-based, replaces duck-typing).
|
|
14
|
+
*/
|
|
15
|
+
const isReplyResult = (value) => value !== null && typeof value === "object" && ReplyResultSymbol in value;
|
|
16
|
+
const DeferReplySymbol = Symbol.for("effect-machine/DeferReply");
|
|
17
|
+
/**
|
|
18
|
+
* Create a deferred reply result. Handler returns this to signal
|
|
19
|
+
* "spawn handler will call self.reply(value) later".
|
|
20
|
+
*/
|
|
21
|
+
const makeDeferReply = (state) => ({
|
|
22
|
+
state,
|
|
23
|
+
[DeferReplySymbol]: true
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Type guard for DeferReplyResult.
|
|
27
|
+
*/
|
|
28
|
+
const isDeferReplyResult = (value) => value !== null && typeof value === "object" && DeferReplySymbol in value;
|
|
3
29
|
/**
|
|
4
30
|
* Internal event tags used for lifecycle effect contexts.
|
|
5
31
|
* Prefixed with $ to distinguish from user events.
|
|
@@ -42,4 +68,4 @@ const stubSystem = {
|
|
|
42
68
|
subscribe: () => () => {}
|
|
43
69
|
};
|
|
44
70
|
//#endregion
|
|
45
|
-
export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isEffect, stubSystem };
|
|
71
|
+
export { INTERNAL_ENTER_EVENT, INTERNAL_INIT_EVENT, getTag, isDeferReplyResult, isEffect, isReplyResult, makeDeferReply, makeReply, stubSystem };
|
package/v3/dist/machine.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EffectHandlers, EffectSlots, EffectsDef, EffectsSchema, GuardHandlers, GuardSlots, GuardsDef, GuardsSchema, MachineContext } from "./slot.js";
|
|
2
|
-
import { TransitionResult } from "./internal/utils.js";
|
|
3
|
-
import { BrandedEvent, BrandedState, TaggedOrConstructor } from "./internal/brands.js";
|
|
2
|
+
import { ReplyResult, TransitionResult } from "./internal/utils.js";
|
|
3
|
+
import { BrandedEvent, BrandedState, ExtractReply, TaggedOrConstructor } from "./internal/brands.js";
|
|
4
4
|
import { MachineEventSchema, MachineStateSchema, VariantsUnion } from "./schema.js";
|
|
5
5
|
import { DuplicateActorError } from "./errors.js";
|
|
6
6
|
import { findTransitions } from "./internal/transition.js";
|
|
@@ -9,7 +9,7 @@ import { Cause, Context, Duration, Effect, Schema, Scope } from "effect";
|
|
|
9
9
|
|
|
10
10
|
//#region src/machine.d.ts
|
|
11
11
|
declare namespace machine_d_exports {
|
|
12
|
-
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, spawn };
|
|
12
|
+
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, make, replay, reply, spawn };
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Self reference for sending events back to the machine
|
|
@@ -45,9 +45,11 @@ interface StateHandlerContext<State, Event, ED extends EffectsDef> {
|
|
|
45
45
|
readonly system: ActorSystem;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Transition handler function
|
|
48
|
+
* Transition handler function.
|
|
49
|
+
* When Reply is concrete (event has a reply schema), handler must return Machine.reply().
|
|
50
|
+
* When Reply is never, handler returns plain state.
|
|
49
51
|
*/
|
|
50
|
-
type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R>;
|
|
52
|
+
type TransitionHandler<S, E, NewState, GD extends GuardsDef, ED extends EffectsDef, R, Reply = never> = (ctx: HandlerContext<S, E, GD, ED>) => TransitionResult<NewState, R, Reply>;
|
|
51
53
|
/**
|
|
52
54
|
* State effect handler function
|
|
53
55
|
*/
|
|
@@ -171,6 +173,8 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
171
173
|
};
|
|
172
174
|
readonly stateSchema?: Schema.Schema<State, unknown, never>;
|
|
173
175
|
readonly eventSchema?: Schema.Schema<Event, unknown, never>;
|
|
176
|
+
/** @internal */
|
|
177
|
+
readonly _replySchemas: ReadonlyMap<string, Schema.Schema.Any>;
|
|
174
178
|
/**
|
|
175
179
|
* Context tag for accessing machine state/event/self in slot handlers.
|
|
176
180
|
* Uses shared module-level tag for all machines.
|
|
@@ -186,24 +190,25 @@ declare class Machine<State, Event, R = never, _SD extends Record<string, Schema
|
|
|
186
190
|
}>;
|
|
187
191
|
get guardsSchema(): GuardsSchema<GD> | undefined;
|
|
188
192
|
get effectsSchema(): EffectsSchema<EFD> | undefined;
|
|
193
|
+
get replySchemas(): ReadonlyMap<string, Schema.Schema.Any>;
|
|
189
194
|
/** @internal */
|
|
190
195
|
constructor(initial: State, stateSchema?: Schema.Schema<State, unknown, never>, eventSchema?: Schema.Schema<Event, unknown, never>, guardsSchema?: GuardsSchema<GD>, effectsSchema?: EffectsSchema<EFD>);
|
|
191
196
|
from<NS extends VariantsUnion<_SD> & BrandedState, R1>(state: TaggedOrConstructor<NS>, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
192
197
|
from<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, R1>(states: NS, build: (scope: TransitionScope<State, Event, R, _SD, _ED, GD, EFD, NS[number] extends TaggedOrConstructor<infer S extends VariantsUnion<_SD> & BrandedState> ? S : never>) => R1): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
193
198
|
/** @internal */
|
|
194
|
-
scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never
|
|
199
|
+
scopeTransition<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: ReadonlyArray<TaggedOrConstructor<NS>>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>, reenter: boolean): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
195
200
|
/** Register transition for a single state */
|
|
196
|
-
on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never
|
|
201
|
+
on<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
197
202
|
/** Register transition for multiple states (handler receives union of state types) */
|
|
198
|
-
on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never
|
|
203
|
+
on<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
199
204
|
/**
|
|
200
205
|
* Like `on()`, but forces onEnter/spawn to run even when transitioning to the same state tag.
|
|
201
206
|
* Use this to restart timers, re-run spawned effects, or reset state-scoped effects.
|
|
202
207
|
*/
|
|
203
208
|
/** Single state */
|
|
204
|
-
reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never
|
|
209
|
+
reenter<NS extends VariantsUnion<_SD> & BrandedState, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(state: TaggedOrConstructor<NS>, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
205
210
|
/** Multiple states */
|
|
206
|
-
reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never
|
|
211
|
+
reenter<NS extends ReadonlyArray<TaggedOrConstructor<VariantsUnion<_SD> & BrandedState>>, NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(states: NS, event: TaggedOrConstructor<NE>, handler: TransitionHandler<NS[number] extends TaggedOrConstructor<infer S> ? S : never, NE, RS, GD, EFD, never, ExtractReply<NE>>): Machine<State, Event, R, _SD, _ED, GD, EFD>;
|
|
207
212
|
/**
|
|
208
213
|
* Register a wildcard transition that fires from any state when no specific transition matches.
|
|
209
214
|
* Specific `.on()` transitions always take priority over `.onAny()`.
|
|
@@ -314,8 +319,8 @@ declare class TransitionScope<State, Event, R, _SD extends Record<string, Schema
|
|
|
314
319
|
private readonly machine;
|
|
315
320
|
private readonly states;
|
|
316
321
|
constructor(machine: Machine<State, Event, R, _SD, _ED, GD, EFD>, states: ReadonlyArray<TaggedOrConstructor<SelectedState>>);
|
|
317
|
-
on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never
|
|
318
|
-
reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never
|
|
322
|
+
on<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
|
|
323
|
+
reenter<NE extends VariantsUnion<_ED> & BrandedEvent, RS extends VariantsUnion<_SD> & BrandedState>(event: TaggedOrConstructor<NE>, handler: TransitionHandler<SelectedState, NE, RS, GD, EFD, never, ExtractReply<NE>>): TransitionScope<State, Event, R, _SD, _ED, GD, EFD, SelectedState>;
|
|
319
324
|
}
|
|
320
325
|
declare const make: typeof Machine.make;
|
|
321
326
|
/**
|
|
@@ -346,5 +351,6 @@ declare const replay: <S extends {
|
|
|
346
351
|
}, R>(machine: BuiltMachine<S, E, R>, events: ReadonlyArray<E>, options?: {
|
|
347
352
|
from?: S;
|
|
348
353
|
}) => Effect.Effect<S, never, R>;
|
|
354
|
+
declare const reply: <State, Reply>(state: State, reply: Reply) => ReplyResult<State, Reply>;
|
|
349
355
|
//#endregion
|
|
350
|
-
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, spawn };
|
|
356
|
+
export { BackgroundEffect, BuiltMachine, HandlerContext, Machine, MachineRef, MakeConfig, ProvideHandlers, type ReplyResult, SlotContext, SpawnEffect, StateEffectHandler, StateHandlerContext, TaskOptions, TimeoutConfig, Transition, TransitionHandler, findTransitions, machine_d_exports, make, replay, reply, spawn };
|
package/v3/dist/machine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { __exportAll } from "./_virtual/_rolldown/runtime.js";
|
|
2
2
|
import { Inspector } from "./inspection.js";
|
|
3
|
-
import { getTag, stubSystem } from "./internal/utils.js";
|
|
3
|
+
import { getTag, makeReply, stubSystem } from "./internal/utils.js";
|
|
4
4
|
import { ProvisionValidationError, SlotProvisionError } from "./errors.js";
|
|
5
5
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
6
6
|
import { MachineContextTag } from "./slot.js";
|
|
@@ -14,6 +14,7 @@ var machine_exports = /* @__PURE__ */ __exportAll({
|
|
|
14
14
|
findTransitions: () => findTransitions,
|
|
15
15
|
make: () => make,
|
|
16
16
|
replay: () => replay,
|
|
17
|
+
reply: () => reply,
|
|
17
18
|
spawn: () => spawn
|
|
18
19
|
});
|
|
19
20
|
const emitTaskInspection = (input) => Effect.flatMap(Effect.serviceOptional(Inspector).pipe(Effect.option), (inspector) => Option.isNone(inspector) ? Effect.void : emitWithTimestamp(inspector.value, (timestamp) => ({
|
|
@@ -69,6 +70,7 @@ var Machine = class Machine {
|
|
|
69
70
|
/** @internal */ _slots;
|
|
70
71
|
stateSchema;
|
|
71
72
|
eventSchema;
|
|
73
|
+
/** @internal */ _replySchemas;
|
|
72
74
|
/**
|
|
73
75
|
* Context tag for accessing machine state/event/self in slot handlers.
|
|
74
76
|
* Uses shared module-level tag for all machines.
|
|
@@ -95,6 +97,9 @@ var Machine = class Machine {
|
|
|
95
97
|
get effectsSchema() {
|
|
96
98
|
return this._effectsSchema;
|
|
97
99
|
}
|
|
100
|
+
get replySchemas() {
|
|
101
|
+
return this._replySchemas;
|
|
102
|
+
}
|
|
98
103
|
/** @internal */
|
|
99
104
|
constructor(initial, stateSchema, eventSchema, guardsSchema, effectsSchema) {
|
|
100
105
|
this.initial = initial;
|
|
@@ -109,6 +114,7 @@ var Machine = class Machine {
|
|
|
109
114
|
this._effectHandlers = /* @__PURE__ */ new Map();
|
|
110
115
|
this.stateSchema = stateSchema;
|
|
111
116
|
this.eventSchema = eventSchema;
|
|
117
|
+
this._replySchemas = eventSchema?._replySchemas ?? /* @__PURE__ */ new Map();
|
|
112
118
|
this._slots = {
|
|
113
119
|
guards: this._guardsSchema !== void 0 ? this._guardsSchema._createSlots((name, params) => Effect.flatMap(Effect.serviceOptional(this.Context).pipe(Effect.orDie), (ctx) => {
|
|
114
120
|
const handler = this._guardHandlers.get(name);
|
|
@@ -371,6 +377,7 @@ var Machine = class Machine {
|
|
|
371
377
|
result._spawnEffects = [...this._spawnEffects];
|
|
372
378
|
result._backgroundEffects = [...this._backgroundEffects];
|
|
373
379
|
result._postponeRules = [...this._postponeRules];
|
|
380
|
+
result._replySchemas = this._replySchemas;
|
|
374
381
|
const anyHandlers = handlers;
|
|
375
382
|
if (this._guardsSchema !== void 0) for (const name of Object.keys(this._guardsSchema.definitions)) result._guardHandlers.set(name, anyHandlers[name]);
|
|
376
383
|
if (this._effectsSchema !== void 0) for (const name of Object.keys(this._effectsSchema.definitions)) result._effectHandlers.set(name, anyHandlers[name]);
|
|
@@ -460,5 +467,6 @@ const replay = Effect.fn("effect-machine.replay")(function* (built, events, opti
|
|
|
460
467
|
}
|
|
461
468
|
return state;
|
|
462
469
|
});
|
|
470
|
+
const reply = makeReply;
|
|
463
471
|
//#endregion
|
|
464
|
-
export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, spawn };
|
|
472
|
+
export { BuiltMachine, Machine, findTransitions, machine_exports, make, replay, reply, spawn };
|