effect-machine 0.9.0 → 0.11.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 +118 -55
- package/dist/actor.d.ts +77 -179
- package/dist/actor.js +161 -113
- package/dist/cluster/entity-machine.js +5 -3
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +8 -1
- package/dist/index.d.ts +4 -8
- package/dist/index.js +2 -7
- package/dist/internal/transition.d.ts +27 -3
- package/dist/internal/transition.js +38 -9
- package/dist/internal/utils.d.ts +7 -2
- package/dist/internal/utils.js +1 -5
- package/dist/machine.d.ts +94 -35
- package/dist/machine.js +128 -13
- package/dist/testing.js +57 -3
- package/package.json +10 -9
- package/v3/dist/actor.d.ts +210 -0
- package/{dist-v3 → v3/dist}/actor.js +198 -117
- package/{dist-v3 → v3/dist}/cluster/entity-machine.d.ts +1 -1
- package/{dist-v3 → v3/dist}/cluster/entity-machine.js +8 -6
- package/{dist-v3 → v3/dist}/cluster/to-entity.d.ts +1 -1
- package/{dist-v3 → v3/dist}/cluster/to-entity.js +1 -1
- package/v3/dist/errors.d.ts +76 -0
- package/{dist-v3 → v3/dist}/errors.js +9 -2
- package/v3/dist/index.d.ts +9 -0
- package/v3/dist/index.js +8 -0
- package/{dist-v3 → v3/dist}/inspection.d.ts +53 -8
- package/v3/dist/inspection.js +156 -0
- package/{dist-v3 → v3/dist}/internal/brands.d.ts +1 -1
- package/{dist-v3 → v3/dist}/internal/inspection.d.ts +1 -1
- package/v3/dist/internal/inspection.js +20 -0
- package/{dist-v3 → v3/dist}/internal/transition.d.ts +35 -11
- package/{dist-v3 → v3/dist}/internal/transition.js +47 -15
- package/{dist-v3 → v3/dist}/internal/utils.d.ts +9 -4
- package/{dist-v3 → v3/dist}/internal/utils.js +2 -6
- package/{dist-v3 → v3/dist}/machine.d.ts +113 -40
- package/{dist-v3 → v3/dist}/machine.js +191 -15
- package/{dist-v3 → v3/dist}/schema.d.ts +1 -1
- package/{dist-v3 → v3/dist}/schema.js +5 -2
- package/{dist-v3 → v3/dist}/slot.d.ts +4 -3
- package/{dist-v3 → v3/dist}/slot.js +1 -1
- package/{dist-v3 → v3/dist}/testing.d.ts +14 -8
- package/{dist-v3 → v3/dist}/testing.js +60 -6
- package/dist/persistence/adapter.d.ts +0 -135
- package/dist/persistence/adapter.js +0 -25
- package/dist/persistence/adapters/in-memory.d.ts +0 -32
- package/dist/persistence/adapters/in-memory.js +0 -174
- package/dist/persistence/index.d.ts +0 -5
- package/dist/persistence/index.js +0 -5
- package/dist/persistence/persistent-actor.d.ts +0 -50
- package/dist/persistence/persistent-actor.js +0 -368
- package/dist/persistence/persistent-machine.d.ts +0 -105
- package/dist/persistence/persistent-machine.js +0 -22
- package/dist-v3/actor.d.ts +0 -291
- package/dist-v3/errors.d.ts +0 -27
- package/dist-v3/index.d.ts +0 -12
- package/dist-v3/index.js +0 -13
- package/dist-v3/inspection.js +0 -48
- package/dist-v3/internal/inspection.js +0 -13
- package/dist-v3/persistence/adapter.d.ts +0 -125
- package/dist-v3/persistence/adapter.js +0 -25
- package/dist-v3/persistence/adapters/in-memory.d.ts +0 -32
- package/dist-v3/persistence/adapters/in-memory.js +0 -174
- package/dist-v3/persistence/index.d.ts +0 -5
- package/dist-v3/persistence/index.js +0 -5
- package/dist-v3/persistence/persistent-actor.d.ts +0 -49
- package/dist-v3/persistence/persistent-actor.js +0 -365
- package/dist-v3/persistence/persistent-machine.d.ts +0 -105
- package/dist-v3/persistence/persistent-machine.js +0 -22
- /package/{dist-v3 → v3/dist}/_virtual/_rolldown/runtime.js +0 -0
- /package/{dist-v3 → v3/dist}/cluster/index.d.ts +0 -0
- /package/{dist-v3 → v3/dist}/cluster/index.js +0 -0
- /package/{dist-v3 → v3/dist}/internal/brands.js +0 -0
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { Inspector } from "./inspection.js";
|
|
2
2
|
import { INTERNAL_INIT_EVENT } from "./internal/utils.js";
|
|
3
|
-
import { DuplicateActorError } from "./errors.js";
|
|
4
|
-
import { isPersistentMachine } from "./persistence/persistent-machine.js";
|
|
5
|
-
import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
3
|
+
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
6
4
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
7
|
-
import {
|
|
8
|
-
import { createPersistentActor, restorePersistentActor } from "./persistence/persistent-actor.js";
|
|
5
|
+
import { processEventCore, resolveTransition, runSpawnEffects, shouldPostpone } from "./internal/transition.js";
|
|
9
6
|
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Runtime, Scope, Stream, SubscriptionRef } from "effect";
|
|
10
|
-
//#region src
|
|
7
|
+
//#region src/actor.ts
|
|
11
8
|
/**
|
|
12
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
13
10
|
*
|
|
@@ -31,10 +28,50 @@ const notifyListeners = (listeners, state) => {
|
|
|
31
28
|
/**
|
|
32
29
|
* Build core ActorRef methods shared between regular and persistent actors.
|
|
33
30
|
*/
|
|
34
|
-
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap) => {
|
|
31
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub) => {
|
|
35
32
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
36
33
|
if (yield* Ref.get(stoppedRef)) return;
|
|
37
|
-
yield* Queue.offer(eventQueue,
|
|
34
|
+
yield* Queue.offer(eventQueue, {
|
|
35
|
+
_tag: "send",
|
|
36
|
+
event
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
const call = Effect.fn("effect-machine.actor.call")(function* (event) {
|
|
40
|
+
if (yield* Ref.get(stoppedRef)) {
|
|
41
|
+
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
42
|
+
return {
|
|
43
|
+
newState: currentState,
|
|
44
|
+
previousState: currentState,
|
|
45
|
+
transitioned: false,
|
|
46
|
+
lifecycleRan: false,
|
|
47
|
+
isFinal: machine.finalStates.has(currentState._tag)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const reply = yield* Deferred.make();
|
|
51
|
+
pendingReplies.add(reply);
|
|
52
|
+
yield* Queue.offer(eventQueue, {
|
|
53
|
+
_tag: "call",
|
|
54
|
+
event,
|
|
55
|
+
reply
|
|
56
|
+
});
|
|
57
|
+
return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))), Effect.catchTag("ActorStoppedError", () => SubscriptionRef.get(stateRef).pipe(Effect.map((currentState) => ({
|
|
58
|
+
newState: currentState,
|
|
59
|
+
previousState: currentState,
|
|
60
|
+
transitioned: false,
|
|
61
|
+
lifecycleRan: false,
|
|
62
|
+
isFinal: machine.finalStates.has(currentState._tag)
|
|
63
|
+
})))));
|
|
64
|
+
});
|
|
65
|
+
const ask = Effect.fn("effect-machine.actor.ask")(function* (event) {
|
|
66
|
+
if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
|
|
67
|
+
const reply = yield* Deferred.make();
|
|
68
|
+
pendingReplies.add(reply);
|
|
69
|
+
yield* Queue.offer(eventQueue, {
|
|
70
|
+
_tag: "ask",
|
|
71
|
+
event,
|
|
72
|
+
reply
|
|
73
|
+
});
|
|
74
|
+
return yield* Deferred.await(reply).pipe(Effect.ensuring(Effect.sync(() => pendingReplies.delete(reply))));
|
|
38
75
|
});
|
|
39
76
|
const snapshot = SubscriptionRef.get(stateRef).pipe(Effect.withSpan("effect-machine.actor.snapshot"));
|
|
40
77
|
const matches = Effect.fn("effect-machine.actor.matches")(function* (tag) {
|
|
@@ -72,30 +109,39 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
72
109
|
return {
|
|
73
110
|
id,
|
|
74
111
|
send,
|
|
112
|
+
cast: send,
|
|
113
|
+
call,
|
|
114
|
+
ask,
|
|
75
115
|
state: stateRef,
|
|
76
116
|
stop,
|
|
77
|
-
stopSync: () => Effect.runFork(stop),
|
|
78
117
|
snapshot,
|
|
79
|
-
snapshotSync: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
80
118
|
matches,
|
|
81
|
-
matchesSync: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
82
119
|
can,
|
|
83
|
-
canSync: (event) => {
|
|
84
|
-
return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
|
|
85
|
-
},
|
|
86
120
|
changes: stateRef.changes,
|
|
121
|
+
transitions: transitionsPubSub !== void 0 ? Stream.fromPubSub(transitionsPubSub) : Stream.empty,
|
|
87
122
|
waitFor,
|
|
88
123
|
awaitFinal,
|
|
89
124
|
sendAndWait,
|
|
90
|
-
sendSync: (event) => {
|
|
91
|
-
if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, event));
|
|
92
|
-
},
|
|
93
125
|
subscribe: (fn) => {
|
|
94
126
|
listeners.add(fn);
|
|
95
127
|
return () => {
|
|
96
128
|
listeners.delete(fn);
|
|
97
129
|
};
|
|
98
130
|
},
|
|
131
|
+
sync: {
|
|
132
|
+
send: (event) => {
|
|
133
|
+
if (!Effect.runSync(Ref.get(stoppedRef))) Effect.runSync(Queue.offer(eventQueue, {
|
|
134
|
+
_tag: "send",
|
|
135
|
+
event
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
stop: () => Effect.runFork(stop),
|
|
139
|
+
snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
140
|
+
matches: (tag) => Effect.runSync(SubscriptionRef.get(stateRef))._tag === tag,
|
|
141
|
+
can: (event) => {
|
|
142
|
+
return resolveTransition(machine, Effect.runSync(SubscriptionRef.get(stateRef)), event) !== void 0;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
99
145
|
system,
|
|
100
146
|
children: childrenMap
|
|
101
147
|
};
|
|
@@ -103,7 +149,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
103
149
|
/**
|
|
104
150
|
* Create and start an actor for a machine
|
|
105
151
|
*/
|
|
106
|
-
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine) {
|
|
152
|
+
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
153
|
+
const initial = options?.initialState ?? machine.initial;
|
|
107
154
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
108
155
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
109
156
|
let system;
|
|
@@ -118,11 +165,16 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
118
165
|
const eventQueue = yield* Queue.unbounded();
|
|
119
166
|
const stoppedRef = yield* Ref.make(false);
|
|
120
167
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
168
|
+
const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
169
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
170
|
+
yield* Queue.offer(eventQueue, {
|
|
171
|
+
_tag: "send",
|
|
172
|
+
event
|
|
173
|
+
});
|
|
174
|
+
});
|
|
121
175
|
const self = {
|
|
122
|
-
send:
|
|
123
|
-
|
|
124
|
-
yield* Queue.offer(eventQueue, event);
|
|
125
|
-
}),
|
|
176
|
+
send: selfSend,
|
|
177
|
+
cast: selfSend,
|
|
126
178
|
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
127
179
|
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
128
180
|
childrenMap.set(childId, child);
|
|
@@ -133,19 +185,20 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
133
185
|
return child;
|
|
134
186
|
})
|
|
135
187
|
};
|
|
136
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state",
|
|
188
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
137
189
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
138
190
|
type: "@machine.spawn",
|
|
139
191
|
actorId: id,
|
|
140
|
-
initialState:
|
|
192
|
+
initialState: initial,
|
|
141
193
|
timestamp
|
|
142
194
|
}));
|
|
143
|
-
const stateRef = yield* SubscriptionRef.make(
|
|
195
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
144
196
|
const listeners = /* @__PURE__ */ new Set();
|
|
145
197
|
const backgroundFibers = [];
|
|
146
198
|
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
147
199
|
const ctx = {
|
|
148
|
-
|
|
200
|
+
actorId: id,
|
|
201
|
+
state: initial,
|
|
149
202
|
event: initEvent,
|
|
150
203
|
self,
|
|
151
204
|
system
|
|
@@ -153,7 +206,8 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
153
206
|
const { effects: effectSlots } = machine._slots;
|
|
154
207
|
for (const bg of machine.backgroundEffects) {
|
|
155
208
|
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
156
|
-
|
|
209
|
+
actorId: id,
|
|
210
|
+
state: initial,
|
|
157
211
|
event: initEvent,
|
|
158
212
|
self,
|
|
159
213
|
effects: effectSlots,
|
|
@@ -162,21 +216,23 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
162
216
|
backgroundFibers.push(fiber);
|
|
163
217
|
}
|
|
164
218
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
165
|
-
yield* runSpawnEffectsWithInspection(machine,
|
|
166
|
-
if (machine.finalStates.has(
|
|
219
|
+
yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
220
|
+
if (machine.finalStates.has(initial._tag)) {
|
|
167
221
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
168
222
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
169
223
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
170
224
|
type: "@machine.stop",
|
|
171
225
|
actorId: id,
|
|
172
|
-
finalState:
|
|
226
|
+
finalState: initial,
|
|
173
227
|
timestamp
|
|
174
228
|
}));
|
|
175
229
|
yield* Ref.set(stoppedRef, true);
|
|
176
230
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
177
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
231
|
+
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Ref.set(stoppedRef, true).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, /* @__PURE__ */ new Set());
|
|
178
232
|
}
|
|
179
|
-
const
|
|
233
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
234
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
235
|
+
const loopFiber = yield* Effect.forkDaemon(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub));
|
|
180
236
|
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
181
237
|
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
182
238
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
@@ -187,31 +243,115 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
187
243
|
}));
|
|
188
244
|
yield* Ref.set(stoppedRef, true);
|
|
189
245
|
yield* Fiber.interrupt(loopFiber);
|
|
246
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
190
247
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
191
248
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
192
249
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
193
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap);
|
|
250
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
|
|
251
|
+
});
|
|
252
|
+
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
253
|
+
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
254
|
+
const error = new ActorStoppedError({ actorId });
|
|
255
|
+
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
256
|
+
pendingReplies.clear();
|
|
194
257
|
});
|
|
195
258
|
/**
|
|
196
|
-
* Main event loop for the actor
|
|
259
|
+
* Main event loop for the actor.
|
|
260
|
+
* Includes postpone buffer — events matching postpone rules are buffered
|
|
261
|
+
* and drained after state tag changes (gen_statem semantics).
|
|
197
262
|
*/
|
|
198
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system) {
|
|
199
|
-
|
|
200
|
-
|
|
263
|
+
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
|
|
264
|
+
const postponed = [];
|
|
265
|
+
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
266
|
+
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
267
|
+
const event = queued.event;
|
|
201
268
|
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
202
|
-
if (
|
|
269
|
+
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
270
|
+
postponed.push(queued);
|
|
271
|
+
if (queued._tag === "call") {
|
|
272
|
+
const postponedResult = {
|
|
273
|
+
newState: currentState,
|
|
274
|
+
previousState: currentState,
|
|
275
|
+
transitioned: false,
|
|
276
|
+
lifecycleRan: false,
|
|
277
|
+
isFinal: false,
|
|
278
|
+
hasReply: false,
|
|
279
|
+
reply: void 0,
|
|
280
|
+
postponed: true
|
|
281
|
+
};
|
|
282
|
+
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
shouldStop: false,
|
|
286
|
+
stateChanged: false
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
203
290
|
"effect_machine.actor.id": actorId,
|
|
204
291
|
"effect_machine.state.current": currentState._tag,
|
|
205
292
|
"effect_machine.event.type": event._tag
|
|
206
|
-
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system))
|
|
293
|
+
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
|
|
294
|
+
switch (queued._tag) {
|
|
295
|
+
case "call":
|
|
296
|
+
yield* Deferred.succeed(queued.reply, result);
|
|
297
|
+
break;
|
|
298
|
+
case "ask":
|
|
299
|
+
if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
|
|
300
|
+
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
301
|
+
actorId,
|
|
302
|
+
eventTag: event._tag
|
|
303
|
+
}));
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
307
|
+
fromState: result.previousState,
|
|
308
|
+
toState: result.newState,
|
|
309
|
+
event
|
|
310
|
+
});
|
|
311
|
+
return {
|
|
312
|
+
shouldStop,
|
|
313
|
+
stateChanged: result.lifecycleRan
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
while (true) {
|
|
317
|
+
const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
|
|
318
|
+
if (shouldStop) {
|
|
207
319
|
yield* Ref.set(stoppedRef, true);
|
|
320
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
321
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
208
322
|
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
209
323
|
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
210
324
|
return;
|
|
211
325
|
}
|
|
326
|
+
let drainTriggered = stateChanged;
|
|
327
|
+
while (drainTriggered && postponed.length > 0) {
|
|
328
|
+
drainTriggered = false;
|
|
329
|
+
const drained = postponed.splice(0);
|
|
330
|
+
for (const entry of drained) {
|
|
331
|
+
const drain = yield* processQueued(entry);
|
|
332
|
+
if (drain.shouldStop) {
|
|
333
|
+
yield* Ref.set(stoppedRef, true);
|
|
334
|
+
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
335
|
+
yield* settlePendingReplies(pendingReplies, actorId);
|
|
336
|
+
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
337
|
+
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (drain.stateChanged) drainTriggered = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
212
343
|
}
|
|
213
344
|
});
|
|
214
345
|
/**
|
|
346
|
+
* Settle all reply-bearing entries in the postpone buffer on shutdown.
|
|
347
|
+
* Call entries already had their Deferred settled with the postponed result
|
|
348
|
+
* (so their pendingReplies entry is already removed). Ask/send entries
|
|
349
|
+
* with Deferreds are settled via the pendingReplies registry.
|
|
350
|
+
*/
|
|
351
|
+
const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
|
|
352
|
+
postponed.length = 0;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
215
355
|
* Process a single event, returning true if the actor should stop.
|
|
216
356
|
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
217
357
|
*/
|
|
@@ -223,7 +363,7 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
|
|
|
223
363
|
event,
|
|
224
364
|
timestamp
|
|
225
365
|
}));
|
|
226
|
-
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, inspector === void 0 ? void 0 : {
|
|
366
|
+
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
|
|
227
367
|
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
228
368
|
type: "@machine.effect",
|
|
229
369
|
actorId,
|
|
@@ -251,7 +391,10 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
|
|
|
251
391
|
});
|
|
252
392
|
if (!result.transitioned) {
|
|
253
393
|
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
254
|
-
return
|
|
394
|
+
return {
|
|
395
|
+
shouldStop: false,
|
|
396
|
+
result
|
|
397
|
+
};
|
|
255
398
|
}
|
|
256
399
|
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
257
400
|
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
@@ -266,10 +409,16 @@ const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (m
|
|
|
266
409
|
finalState: result.newState,
|
|
267
410
|
timestamp
|
|
268
411
|
}));
|
|
269
|
-
return
|
|
412
|
+
return {
|
|
413
|
+
shouldStop: true,
|
|
414
|
+
result
|
|
415
|
+
};
|
|
270
416
|
}
|
|
271
417
|
}
|
|
272
|
-
return
|
|
418
|
+
return {
|
|
419
|
+
shouldStop: false,
|
|
420
|
+
result
|
|
421
|
+
};
|
|
273
422
|
});
|
|
274
423
|
/**
|
|
275
424
|
* Run spawn effects with actor-specific inspection and tracing.
|
|
@@ -284,7 +433,7 @@ const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffec
|
|
|
284
433
|
state,
|
|
285
434
|
timestamp
|
|
286
435
|
}));
|
|
287
|
-
yield* runSpawnEffects(machine, state, event, self, stateScope, system, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
436
|
+
yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
288
437
|
type: "@machine.error",
|
|
289
438
|
actorId,
|
|
290
439
|
phase: info.phase,
|
|
@@ -305,13 +454,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
305
454
|
const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
|
|
306
455
|
const eventPubSub = yield* PubSub.unbounded();
|
|
307
456
|
const eventListeners = /* @__PURE__ */ new Set();
|
|
308
|
-
const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.
|
|
457
|
+
const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.zipRight(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
|
|
309
458
|
yield* Effect.addFinalizer(() => {
|
|
310
459
|
const stops = [];
|
|
311
460
|
MutableHashMap.forEach(actorsMap, (actor) => {
|
|
312
461
|
stops.push(actor.stop);
|
|
313
462
|
});
|
|
314
|
-
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.
|
|
463
|
+
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.zipRight(PubSub.shutdown(eventPubSub)), Effect.asVoid);
|
|
315
464
|
});
|
|
316
465
|
/** Check for duplicate ID, register actor, attach scope cleanup if available */
|
|
317
466
|
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
|
|
@@ -344,25 +493,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
344
493
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
345
494
|
return yield* registerActor(id, yield* createActor(id, built._inner));
|
|
346
495
|
});
|
|
347
|
-
const
|
|
348
|
-
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
349
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
350
|
-
const maybeSnapshot = yield* adapter.loadSnapshot(id, persistentMachine.persistence.stateSchema);
|
|
351
|
-
return yield* registerActor(id, yield* createPersistentActor(id, persistentMachine, maybeSnapshot, yield* adapter.loadEvents(id, persistentMachine.persistence.eventSchema, Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : void 0)));
|
|
352
|
-
});
|
|
353
|
-
const spawnImpl = Effect.fn("effect-machine.actorSystem.spawn")(function* (id, machine) {
|
|
354
|
-
if (isPersistentMachine(machine)) return yield* spawnPersistent(id, machine);
|
|
355
|
-
return yield* spawnRegular(id, machine);
|
|
356
|
-
});
|
|
357
|
-
function spawn(id, machine) {
|
|
358
|
-
return withSpawnGate(spawnImpl(id, machine));
|
|
359
|
-
}
|
|
360
|
-
const restoreImpl = Effect.fn("effect-machine.actorSystem.restore")(function* (id, persistentMachine) {
|
|
361
|
-
const maybeActor = yield* restorePersistentActor(id, persistentMachine);
|
|
362
|
-
if (Option.isSome(maybeActor)) yield* registerActor(id, maybeActor.value);
|
|
363
|
-
return maybeActor;
|
|
364
|
-
});
|
|
365
|
-
const restore = (id, persistentMachine) => withSpawnGate(restoreImpl(id, persistentMachine));
|
|
496
|
+
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
366
497
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
367
498
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
368
499
|
});
|
|
@@ -379,55 +510,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
379
510
|
yield* actor.stop;
|
|
380
511
|
return true;
|
|
381
512
|
});
|
|
382
|
-
const listPersisted = Effect.fn("effect-machine.actorSystem.listPersisted")(function* () {
|
|
383
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
384
|
-
if (adapter.listActors === void 0) return [];
|
|
385
|
-
return yield* adapter.listActors();
|
|
386
|
-
});
|
|
387
|
-
const restoreMany = Effect.fn("effect-machine.actorSystem.restoreMany")(function* (ids, persistentMachine) {
|
|
388
|
-
const restored = [];
|
|
389
|
-
const failed = [];
|
|
390
|
-
for (const id of ids) {
|
|
391
|
-
if (MutableHashMap.has(actorsMap, id)) continue;
|
|
392
|
-
const result = yield* Effect.either(restore(id, persistentMachine));
|
|
393
|
-
if (result._tag === "Left") failed.push({
|
|
394
|
-
id,
|
|
395
|
-
error: result.left
|
|
396
|
-
});
|
|
397
|
-
else if (Option.isSome(result.right)) restored.push(result.right.value);
|
|
398
|
-
else failed.push({
|
|
399
|
-
id,
|
|
400
|
-
error: new PersistenceError({
|
|
401
|
-
operation: "restore",
|
|
402
|
-
actorId: id,
|
|
403
|
-
message: "No persisted state found"
|
|
404
|
-
})
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
return {
|
|
408
|
-
restored,
|
|
409
|
-
failed
|
|
410
|
-
};
|
|
411
|
-
});
|
|
412
|
-
const restoreAll = Effect.fn("effect-machine.actorSystem.restoreAll")(function* (persistentMachine, options) {
|
|
413
|
-
const adapter = yield* PersistenceAdapterTag;
|
|
414
|
-
if (adapter.listActors === void 0) return {
|
|
415
|
-
restored: [],
|
|
416
|
-
failed: []
|
|
417
|
-
};
|
|
418
|
-
const machineType = persistentMachine.persistence.machineType;
|
|
419
|
-
if (machineType === void 0) return yield* new PersistenceError({
|
|
420
|
-
operation: "restoreAll",
|
|
421
|
-
actorId: "*",
|
|
422
|
-
message: "restoreAll requires explicit machineType in persistence config"
|
|
423
|
-
});
|
|
424
|
-
let filtered = (yield* adapter.listActors()).filter((meta) => meta.machineType === machineType);
|
|
425
|
-
if (options?.filter !== void 0) filtered = filtered.filter(options.filter);
|
|
426
|
-
return yield* restoreMany(filtered.map((meta) => meta.id), persistentMachine);
|
|
427
|
-
});
|
|
428
513
|
return ActorSystem.of({
|
|
429
514
|
spawn,
|
|
430
|
-
restore,
|
|
431
515
|
get,
|
|
432
516
|
stop,
|
|
433
517
|
events: Stream.fromPubSub(eventPubSub),
|
|
@@ -443,10 +527,7 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
443
527
|
return () => {
|
|
444
528
|
eventListeners.delete(fn);
|
|
445
529
|
};
|
|
446
|
-
}
|
|
447
|
-
listPersisted,
|
|
448
|
-
restoreMany,
|
|
449
|
-
restoreAll
|
|
530
|
+
}
|
|
450
531
|
});
|
|
451
532
|
});
|
|
452
533
|
/**
|
|
@@ -454,4 +535,4 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
454
535
|
*/
|
|
455
536
|
const Default = Layer.scoped(ActorSystem, make());
|
|
456
537
|
//#endregion
|
|
457
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects };
|
|
538
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
@@ -2,7 +2,7 @@ import { processEventCore, runSpawnEffects } from "../internal/transition.js";
|
|
|
2
2
|
import { ActorSystem } from "../actor.js";
|
|
3
3
|
import { Effect, Option, Queue, Ref, Scope } from "effect";
|
|
4
4
|
import { Entity } from "@effect/cluster";
|
|
5
|
-
//#region src
|
|
5
|
+
//#region src/cluster/entity-machine.ts
|
|
6
6
|
/**
|
|
7
7
|
* EntityMachine adapter - wires a machine to a cluster Entity layer.
|
|
8
8
|
*
|
|
@@ -13,7 +13,7 @@ import { Entity } from "@effect/cluster";
|
|
|
13
13
|
* Returns the new state after processing.
|
|
14
14
|
*/
|
|
15
15
|
const processEvent = Effect.fn("effect-machine.cluster.processEvent")(function* (machine, stateRef, event, self, stateScopeRef, system, hooks) {
|
|
16
|
-
const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, hooks);
|
|
16
|
+
const result = yield* processEventCore(machine, yield* Ref.get(stateRef), event, self, stateScopeRef, system, "*", hooks);
|
|
17
17
|
if (result.transitioned) yield* Ref.set(stateRef, result.newState);
|
|
18
18
|
return result.newState;
|
|
19
19
|
});
|
|
@@ -54,15 +54,17 @@ const EntityMachine = { layer: (entity, machine, options) => {
|
|
|
54
54
|
if (Option.isNone(existingSystem)) return yield* Effect.die("EntityMachine requires ActorSystem in context");
|
|
55
55
|
const system = existingSystem.value;
|
|
56
56
|
const internalQueue = yield* Queue.unbounded();
|
|
57
|
+
const clusterSend = Effect.fn("effect-machine.cluster.self.send")(function* (event) {
|
|
58
|
+
yield* Queue.offer(internalQueue, event);
|
|
59
|
+
});
|
|
57
60
|
const self = {
|
|
58
|
-
send:
|
|
59
|
-
|
|
60
|
-
}),
|
|
61
|
+
send: clusterSend,
|
|
62
|
+
cast: clusterSend,
|
|
61
63
|
spawn: (childId, childMachine) => system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system))
|
|
62
64
|
};
|
|
63
65
|
const stateRef = yield* Ref.make(initialState);
|
|
64
66
|
const stateScopeRef = { current: yield* Scope.make() };
|
|
65
|
-
yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, system, options?.hooks?.onError);
|
|
67
|
+
yield* runSpawnEffects(machine, initialState, { _tag: "$init" }, self, stateScopeRef.current, system, entityId, options?.hooks?.onError);
|
|
66
68
|
const runInternalEvent = Effect.fn("effect-machine.cluster.internalEvent")(function* () {
|
|
67
69
|
yield* processEvent(machine, stateRef, yield* Queue.take(internalQueue), self, stateScopeRef, system, options?.hooks);
|
|
68
70
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
//#region src/errors.d.ts
|
|
4
|
+
declare const DuplicateActorError_base: Schema.TaggedErrorClass<DuplicateActorError, "DuplicateActorError", {
|
|
5
|
+
readonly _tag: Schema.tag<"DuplicateActorError">;
|
|
6
|
+
} & {
|
|
7
|
+
actorId: typeof Schema.String;
|
|
8
|
+
}>;
|
|
9
|
+
/** Attempted to spawn/restore actor with ID already in use */
|
|
10
|
+
declare class DuplicateActorError extends DuplicateActorError_base {}
|
|
11
|
+
declare const UnprovidedSlotsError_base: Schema.TaggedErrorClass<UnprovidedSlotsError, "UnprovidedSlotsError", {
|
|
12
|
+
readonly _tag: Schema.tag<"UnprovidedSlotsError">;
|
|
13
|
+
} & {
|
|
14
|
+
slots: Schema.Array$<typeof Schema.String>;
|
|
15
|
+
}>;
|
|
16
|
+
/** Machine has unprovided effect slots */
|
|
17
|
+
declare class UnprovidedSlotsError extends UnprovidedSlotsError_base {}
|
|
18
|
+
declare const MissingSchemaError_base: Schema.TaggedErrorClass<MissingSchemaError, "MissingSchemaError", {
|
|
19
|
+
readonly _tag: Schema.tag<"MissingSchemaError">;
|
|
20
|
+
} & {
|
|
21
|
+
operation: typeof Schema.String;
|
|
22
|
+
}>;
|
|
23
|
+
/** Operation requires schemas attached to machine */
|
|
24
|
+
declare class MissingSchemaError extends MissingSchemaError_base {}
|
|
25
|
+
declare const InvalidSchemaError_base: Schema.TaggedErrorClass<InvalidSchemaError, "InvalidSchemaError", {
|
|
26
|
+
readonly _tag: Schema.tag<"InvalidSchemaError">;
|
|
27
|
+
}>;
|
|
28
|
+
/** State/Event schema has no variants */
|
|
29
|
+
declare class InvalidSchemaError extends InvalidSchemaError_base {}
|
|
30
|
+
declare const MissingMatchHandlerError_base: Schema.TaggedErrorClass<MissingMatchHandlerError, "MissingMatchHandlerError", {
|
|
31
|
+
readonly _tag: Schema.tag<"MissingMatchHandlerError">;
|
|
32
|
+
} & {
|
|
33
|
+
tag: typeof Schema.String;
|
|
34
|
+
}>;
|
|
35
|
+
/** $match called with missing handler for tag */
|
|
36
|
+
declare class MissingMatchHandlerError extends MissingMatchHandlerError_base {}
|
|
37
|
+
declare const SlotProvisionError_base: Schema.TaggedErrorClass<SlotProvisionError, "SlotProvisionError", {
|
|
38
|
+
readonly _tag: Schema.tag<"SlotProvisionError">;
|
|
39
|
+
} & {
|
|
40
|
+
slotName: typeof Schema.String;
|
|
41
|
+
slotType: Schema.Literal<["guard", "effect"]>;
|
|
42
|
+
}>;
|
|
43
|
+
/** Slot handler not found at runtime (internal error) */
|
|
44
|
+
declare class SlotProvisionError extends SlotProvisionError_base {}
|
|
45
|
+
declare const ProvisionValidationError_base: Schema.TaggedErrorClass<ProvisionValidationError, "ProvisionValidationError", {
|
|
46
|
+
readonly _tag: Schema.tag<"ProvisionValidationError">;
|
|
47
|
+
} & {
|
|
48
|
+
missing: Schema.Array$<typeof Schema.String>;
|
|
49
|
+
extra: Schema.Array$<typeof Schema.String>;
|
|
50
|
+
}>;
|
|
51
|
+
/** Machine.build() validation failed - missing or extra handlers */
|
|
52
|
+
declare class ProvisionValidationError extends ProvisionValidationError_base {}
|
|
53
|
+
declare const AssertionError_base: Schema.TaggedErrorClass<AssertionError, "AssertionError", {
|
|
54
|
+
readonly _tag: Schema.tag<"AssertionError">;
|
|
55
|
+
} & {
|
|
56
|
+
message: typeof Schema.String;
|
|
57
|
+
}>;
|
|
58
|
+
/** Assertion failed in testing utilities */
|
|
59
|
+
declare class AssertionError extends AssertionError_base {}
|
|
60
|
+
declare const ActorStoppedError_base: Schema.TaggedErrorClass<ActorStoppedError, "ActorStoppedError", {
|
|
61
|
+
readonly _tag: Schema.tag<"ActorStoppedError">;
|
|
62
|
+
} & {
|
|
63
|
+
actorId: typeof Schema.String;
|
|
64
|
+
}>;
|
|
65
|
+
/** Actor was stopped while a call/ask was pending */
|
|
66
|
+
declare class ActorStoppedError extends ActorStoppedError_base {}
|
|
67
|
+
declare const NoReplyError_base: Schema.TaggedErrorClass<NoReplyError, "NoReplyError", {
|
|
68
|
+
readonly _tag: Schema.tag<"NoReplyError">;
|
|
69
|
+
} & {
|
|
70
|
+
actorId: typeof Schema.String;
|
|
71
|
+
eventTag: typeof Schema.String;
|
|
72
|
+
}>;
|
|
73
|
+
/** ask() was used but the transition handler did not call reply */
|
|
74
|
+
declare class NoReplyError extends NoReplyError_base {}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
//#region src
|
|
2
|
+
//#region src/errors.ts
|
|
3
3
|
/**
|
|
4
4
|
* Typed error classes for effect-machine.
|
|
5
5
|
*
|
|
@@ -32,5 +32,12 @@ var ProvisionValidationError = class extends Schema.TaggedError()("ProvisionVali
|
|
|
32
32
|
}) {};
|
|
33
33
|
/** Assertion failed in testing utilities */
|
|
34
34
|
var AssertionError = class extends Schema.TaggedError()("AssertionError", { message: Schema.String }) {};
|
|
35
|
+
/** Actor was stopped while a call/ask was pending */
|
|
36
|
+
var ActorStoppedError = class extends Schema.TaggedError()("ActorStoppedError", { actorId: Schema.String }) {};
|
|
37
|
+
/** ask() was used but the transition handler did not call reply */
|
|
38
|
+
var NoReplyError = class extends Schema.TaggedError()("NoReplyError", {
|
|
39
|
+
actorId: Schema.String,
|
|
40
|
+
eventTag: Schema.String
|
|
41
|
+
}) {};
|
|
35
42
|
//#endregion
|
|
36
|
-
export { AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
|
|
43
|
+
export { ActorStoppedError, AssertionError, DuplicateActorError, InvalidSchemaError, MissingMatchHandlerError, MissingSchemaError, NoReplyError, ProvisionValidationError, SlotProvisionError, UnprovidedSlotsError };
|