effect-machine 0.12.0 → 0.13.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 +128 -324
- package/dist/actor.d.ts +42 -27
- package/dist/actor.js +212 -305
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +2 -2
- package/dist/errors.d.ts +16 -19
- package/dist/errors.js +3 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -3
- package/dist/internal/runtime.d.ts +81 -6
- package/dist/internal/runtime.js +166 -57
- package/dist/internal/transition.d.ts +5 -4
- package/dist/internal/transition.js +9 -9
- package/dist/machine.d.ts +65 -44
- package/dist/machine.js +68 -67
- package/dist/schema.js +1 -1
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +17 -8
- package/dist/testing.js +22 -25
- package/package.json +5 -5
- package/v3/dist/actor.d.ts +50 -34
- package/v3/dist/actor.js +209 -289
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/errors.d.ts +3 -8
- package/v3/dist/errors.js +2 -4
- package/v3/dist/index.d.ts +5 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/runtime.d.ts +82 -5
- package/v3/dist/internal/runtime.js +147 -48
- package/v3/dist/internal/transition.d.ts +5 -4
- package/v3/dist/internal/transition.js +8 -8
- package/v3/dist/machine.d.ts +18 -36
- package/v3/dist/machine.js +54 -64
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +18 -9
- package/v3/dist/testing.js +21 -22
package/v3/dist/actor.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ActorStoppedError, DuplicateActorError, NoReplyError } from "./errors.js";
|
|
1
|
+
import { ActorStoppedError, DuplicateActorError } from "./errors.js";
|
|
2
|
+
import { processEventCore, resolveTransition, runSpawnEffects } from "./internal/transition.js";
|
|
4
3
|
import { emitWithTimestamp } from "./internal/inspection.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { Inspector } from "./inspection.js";
|
|
5
|
+
import { materializeMachine } from "./machine.js";
|
|
6
|
+
import { createRuntime } from "./internal/runtime.js";
|
|
7
|
+
import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Stream, SubscriptionRef } from "effect";
|
|
7
8
|
//#region src/actor.ts
|
|
8
9
|
/**
|
|
9
10
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -11,7 +12,7 @@ import { Cause, Context, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, O
|
|
|
11
12
|
* Combines:
|
|
12
13
|
* - ActorRef interface (running actor handle)
|
|
13
14
|
* - ActorSystem service (spawn/stop/get actors)
|
|
14
|
-
* - Actor creation
|
|
15
|
+
* - Actor creation (delegates to runtime kernel)
|
|
15
16
|
*/
|
|
16
17
|
/**
|
|
17
18
|
* ActorSystem service tag
|
|
@@ -26,12 +27,13 @@ const notifyListeners = (listeners, state) => {
|
|
|
26
27
|
} catch {}
|
|
27
28
|
};
|
|
28
29
|
/**
|
|
29
|
-
* Build core ActorRef methods
|
|
30
|
+
* Build core ActorRef methods.
|
|
30
31
|
*/
|
|
31
|
-
const buildActorRefCore = (id, machine, stateRef,
|
|
32
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
|
|
32
33
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
33
34
|
if (yield* Ref.get(stoppedRef)) return;
|
|
34
|
-
yield*
|
|
35
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
36
|
+
yield* Queue.offer(q, {
|
|
35
37
|
_tag: "send",
|
|
36
38
|
event
|
|
37
39
|
});
|
|
@@ -49,7 +51,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
49
51
|
}
|
|
50
52
|
const reply = yield* Deferred.make();
|
|
51
53
|
pendingReplies.add(reply);
|
|
52
|
-
yield*
|
|
54
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
55
|
+
yield* Queue.offer(q, {
|
|
53
56
|
_tag: "call",
|
|
54
57
|
event,
|
|
55
58
|
reply
|
|
@@ -66,7 +69,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
66
69
|
if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
|
|
67
70
|
const reply = yield* Deferred.make();
|
|
68
71
|
pendingReplies.add(reply);
|
|
69
|
-
yield*
|
|
72
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
73
|
+
yield* Queue.offer(q, {
|
|
70
74
|
_tag: "ask",
|
|
71
75
|
event,
|
|
72
76
|
reply
|
|
@@ -85,10 +89,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
85
89
|
const current = yield* SubscriptionRef.get(stateRef);
|
|
86
90
|
if (predicate(current)) return current;
|
|
87
91
|
const done = yield* Deferred.make();
|
|
88
|
-
const rt = yield* Effect.runtime();
|
|
89
|
-
const runFork = Runtime.runFork(rt);
|
|
90
92
|
const listener = (state) => {
|
|
91
|
-
if (predicate(state)) runFork(Deferred.succeed(done, state));
|
|
93
|
+
if (predicate(state)) Effect.runFork(Deferred.succeed(done, state));
|
|
92
94
|
};
|
|
93
95
|
listeners.add(listener);
|
|
94
96
|
const afterSubscribe = yield* SubscriptionRef.get(stateRef);
|
|
@@ -128,12 +130,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
128
130
|
listeners.delete(fn);
|
|
129
131
|
};
|
|
130
132
|
},
|
|
133
|
+
awaitExit: Deferred.await(exitDeferred),
|
|
134
|
+
watch: (other) => other.awaitExit,
|
|
135
|
+
drain: Effect.gen(function* () {
|
|
136
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
137
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
138
|
+
const done = yield* Deferred.make();
|
|
139
|
+
yield* Queue.offer(q, {
|
|
140
|
+
_tag: "drain",
|
|
141
|
+
done
|
|
142
|
+
});
|
|
143
|
+
yield* Deferred.await(done);
|
|
144
|
+
}).pipe(Effect.asVoid),
|
|
131
145
|
sync: {
|
|
132
146
|
send: (event) => {
|
|
133
|
-
if (!Effect.runSync(Ref.get(stoppedRef)))
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
if (!Effect.runSync(Ref.get(stoppedRef))) {
|
|
148
|
+
const q = Effect.runSync(Ref.get(eventQueueRef));
|
|
149
|
+
Effect.runSync(Queue.offer(q, {
|
|
150
|
+
_tag: "send",
|
|
151
|
+
event
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
137
154
|
},
|
|
138
155
|
stop: () => Effect.runFork(stop),
|
|
139
156
|
snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
@@ -147,11 +164,13 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
147
164
|
};
|
|
148
165
|
};
|
|
149
166
|
/**
|
|
150
|
-
* Create and start an actor for a machine
|
|
167
|
+
* Create and start an actor for a machine.
|
|
168
|
+
* Uses the shared runtime kernel with lifecycle hooks for actor-specific concerns.
|
|
151
169
|
*/
|
|
152
170
|
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
153
171
|
const initial = options?.initialState ?? machine.initial;
|
|
154
172
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
173
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
155
174
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
156
175
|
let system;
|
|
157
176
|
let implicitSystemScope;
|
|
@@ -162,298 +181,180 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
162
181
|
implicitSystemScope = scope;
|
|
163
182
|
}
|
|
164
183
|
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
165
|
-
const eventQueue = yield* Queue.unbounded();
|
|
166
|
-
const stoppedRef = yield* Ref.make(false);
|
|
167
184
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
_tag: "send",
|
|
172
|
-
event
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
const self = {
|
|
176
|
-
send: selfSend,
|
|
177
|
-
cast: selfSend,
|
|
178
|
-
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
179
|
-
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
180
|
-
childrenMap.set(childId, child);
|
|
181
|
-
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
182
|
-
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
183
|
-
childrenMap.delete(childId);
|
|
184
|
-
}));
|
|
185
|
-
return child;
|
|
186
|
-
})
|
|
187
|
-
};
|
|
188
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
185
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
186
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
187
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
189
188
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
190
189
|
type: "@machine.spawn",
|
|
191
190
|
actorId: id,
|
|
192
191
|
initialState: initial,
|
|
193
192
|
timestamp
|
|
194
193
|
}));
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const backgroundFibers = [];
|
|
198
|
-
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
199
|
-
const ctx = {
|
|
200
|
-
actorId: id,
|
|
201
|
-
state: initial,
|
|
202
|
-
event: initEvent,
|
|
203
|
-
self,
|
|
204
|
-
system
|
|
205
|
-
};
|
|
206
|
-
const { effects: effectSlots } = machine._slots;
|
|
207
|
-
for (const bg of machine.backgroundEffects) {
|
|
208
|
-
const fiber = yield* Effect.forkDaemon(bg.handler({
|
|
209
|
-
actorId: id,
|
|
210
|
-
state: initial,
|
|
211
|
-
event: initEvent,
|
|
212
|
-
self,
|
|
213
|
-
effects: effectSlots,
|
|
214
|
-
system
|
|
215
|
-
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
216
|
-
backgroundFibers.push(fiber);
|
|
217
|
-
}
|
|
218
|
-
const stateScopeRef = { current: yield* Scope.make() };
|
|
219
|
-
yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
220
|
-
if (machine.finalStates.has(initial._tag)) {
|
|
221
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
222
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
223
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
224
|
-
type: "@machine.stop",
|
|
225
|
-
actorId: id,
|
|
226
|
-
finalState: initial,
|
|
227
|
-
timestamp
|
|
228
|
-
}));
|
|
229
|
-
yield* Ref.set(stoppedRef, true);
|
|
230
|
-
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
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());
|
|
232
|
-
}
|
|
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));
|
|
236
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
237
|
-
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
238
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
239
|
-
type: "@machine.stop",
|
|
240
|
-
actorId: id,
|
|
241
|
-
finalState,
|
|
242
|
-
timestamp
|
|
243
|
-
}));
|
|
244
|
-
yield* Ref.set(stoppedRef, true);
|
|
245
|
-
yield* Fiber.interrupt(loopFiber);
|
|
246
|
-
yield* settlePendingReplies(pendingReplies, id);
|
|
247
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
248
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
249
|
-
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
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();
|
|
257
|
-
});
|
|
258
|
-
/**
|
|
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).
|
|
262
|
-
*/
|
|
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;
|
|
268
|
-
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
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
|
-
deferReply: false,
|
|
280
|
-
reply: void 0,
|
|
281
|
-
postponed: true
|
|
282
|
-
};
|
|
283
|
-
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
shouldStop: false,
|
|
287
|
-
stateChanged: false
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
291
|
-
"effect_machine.actor.id": actorId,
|
|
292
|
-
"effect_machine.state.current": currentState._tag,
|
|
293
|
-
"effect_machine.event.type": event._tag
|
|
294
|
-
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
|
|
295
|
-
switch (queued._tag) {
|
|
296
|
-
case "call":
|
|
297
|
-
yield* Deferred.succeed(queued.reply, result);
|
|
298
|
-
break;
|
|
299
|
-
case "ask":
|
|
300
|
-
if (result.hasReply) {
|
|
301
|
-
const replySchema = machine._replySchemas?.get(event._tag);
|
|
302
|
-
if (replySchema !== void 0) {
|
|
303
|
-
let decoded;
|
|
304
|
-
try {
|
|
305
|
-
decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
|
|
306
|
-
} catch (decodeError) {
|
|
307
|
-
yield* Deferred.die(queued.reply, decodeError);
|
|
308
|
-
return yield* Effect.die(decodeError);
|
|
309
|
-
}
|
|
310
|
-
yield* Deferred.succeed(queued.reply, decoded);
|
|
311
|
-
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
312
|
-
} else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
313
|
-
actorId,
|
|
314
|
-
eventTag: event._tag
|
|
315
|
-
}));
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
319
|
-
fromState: result.previousState,
|
|
320
|
-
toState: result.newState,
|
|
321
|
-
event
|
|
322
|
-
});
|
|
323
|
-
return {
|
|
324
|
-
shouldStop,
|
|
325
|
-
stateChanged: result.lifecycleRan
|
|
326
|
-
};
|
|
327
|
-
});
|
|
328
|
-
while (true) {
|
|
329
|
-
const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
|
|
330
|
-
if (shouldStop) {
|
|
331
|
-
yield* Ref.set(stoppedRef, true);
|
|
332
|
-
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
333
|
-
yield* settlePendingReplies(pendingReplies, actorId);
|
|
334
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
335
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
336
|
-
return;
|
|
337
|
-
}
|
|
338
|
-
let drainTriggered = stateChanged;
|
|
339
|
-
while (drainTriggered && postponed.length > 0) {
|
|
340
|
-
drainTriggered = false;
|
|
341
|
-
const drained = postponed.splice(0);
|
|
342
|
-
for (const entry of drained) {
|
|
343
|
-
const drain = yield* processQueued(entry);
|
|
344
|
-
if (drain.shouldStop) {
|
|
345
|
-
yield* Ref.set(stoppedRef, true);
|
|
346
|
-
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
347
|
-
yield* settlePendingReplies(pendingReplies, actorId);
|
|
348
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
349
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (drain.stateChanged) drainTriggered = true;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
/**
|
|
358
|
-
* Settle all reply-bearing entries in the postpone buffer on shutdown.
|
|
359
|
-
* Call entries already had their Deferred settled with the postponed result
|
|
360
|
-
* (so their pendingReplies entry is already removed). Ask/send entries
|
|
361
|
-
* with Deferreds are settled via the pendingReplies registry.
|
|
362
|
-
*/
|
|
363
|
-
const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
|
|
364
|
-
postponed.length = 0;
|
|
365
|
-
};
|
|
366
|
-
/**
|
|
367
|
-
* Process a single event, returning true if the actor should stop.
|
|
368
|
-
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
369
|
-
*/
|
|
370
|
-
const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
|
|
371
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
372
|
-
type: "@machine.event",
|
|
373
|
-
actorId,
|
|
374
|
-
state: currentState,
|
|
375
|
-
event,
|
|
376
|
-
timestamp
|
|
377
|
-
}));
|
|
378
|
-
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
|
|
379
|
-
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
194
|
+
const hooks = inspectorValue === void 0 ? void 0 : {
|
|
195
|
+
onSpawnEffect: (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
380
196
|
type: "@machine.effect",
|
|
381
|
-
actorId,
|
|
197
|
+
actorId: id,
|
|
382
198
|
effectType: "spawn",
|
|
383
199
|
state,
|
|
384
200
|
timestamp
|
|
385
201
|
})),
|
|
386
|
-
onTransition: (from, to, ev) => emitWithTimestamp(
|
|
202
|
+
onTransition: (from, to, ev) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
387
203
|
type: "@machine.transition",
|
|
388
|
-
actorId,
|
|
204
|
+
actorId: id,
|
|
389
205
|
fromState: from,
|
|
390
206
|
toState: to,
|
|
391
207
|
event: ev,
|
|
392
208
|
timestamp
|
|
393
209
|
})),
|
|
394
|
-
onError: (info) => emitWithTimestamp(
|
|
210
|
+
onError: (info) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
395
211
|
type: "@machine.error",
|
|
396
|
-
actorId,
|
|
212
|
+
actorId: id,
|
|
397
213
|
phase: info.phase,
|
|
398
214
|
state: info.state,
|
|
399
215
|
event: info.event,
|
|
400
216
|
error: Cause.pretty(info.cause),
|
|
401
217
|
timestamp
|
|
402
218
|
}))
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
219
|
+
};
|
|
220
|
+
const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
|
|
221
|
+
value: initial,
|
|
222
|
+
enumerable: true
|
|
223
|
+
} }) : machine;
|
|
224
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
225
|
+
const stoppedRef = yield* Ref.make(false);
|
|
226
|
+
const initialQueue = yield* Queue.unbounded();
|
|
227
|
+
const eventQueueRef = yield* Ref.make(initialQueue);
|
|
228
|
+
const terminalExitDeferred = yield* Deferred.make();
|
|
229
|
+
let stopEmitted = false;
|
|
230
|
+
const runtimeRef = { current: void 0 };
|
|
231
|
+
/** Build lifecycle hooks for a generation */
|
|
232
|
+
const buildLifecycle = () => {
|
|
233
|
+
stopEmitted = false;
|
|
406
234
|
return {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
413
|
-
notifyListeners(listeners, result.newState);
|
|
414
|
-
if (result.lifecycleRan) {
|
|
415
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
416
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
417
|
-
if (result.isFinal) {
|
|
418
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
419
|
-
type: "@machine.stop",
|
|
420
|
-
actorId,
|
|
421
|
-
finalState: result.newState,
|
|
235
|
+
onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
236
|
+
type: "@machine.event",
|
|
237
|
+
actorId: id,
|
|
238
|
+
state,
|
|
239
|
+
event,
|
|
422
240
|
timestamp
|
|
241
|
+
})) : void 0,
|
|
242
|
+
onStateChange: (result, _event) => Effect.gen(function* () {
|
|
243
|
+
notifyListeners(listeners, result.newState);
|
|
244
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
245
|
+
if (result.lifecycleRan) {
|
|
246
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
247
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
248
|
+
}
|
|
249
|
+
}),
|
|
250
|
+
onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
|
|
251
|
+
fromState: result.previousState,
|
|
252
|
+
toState: result.newState,
|
|
253
|
+
event
|
|
254
|
+
}).pipe(Effect.asVoid) : Effect.void,
|
|
255
|
+
onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
|
|
256
|
+
stopEmitted = true;
|
|
257
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
258
|
+
type: "@machine.stop",
|
|
259
|
+
actorId: id,
|
|
260
|
+
finalState: state,
|
|
261
|
+
timestamp
|
|
262
|
+
}));
|
|
263
|
+
}) : void 0,
|
|
264
|
+
onShutdown: () => Effect.gen(function* () {
|
|
265
|
+
if (!stopEmitted) {
|
|
266
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
267
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
268
|
+
type: "@machine.stop",
|
|
269
|
+
actorId: id,
|
|
270
|
+
finalState,
|
|
271
|
+
timestamp
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
275
|
+
}),
|
|
276
|
+
onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
277
|
+
type: "@machine.effect",
|
|
278
|
+
actorId: id,
|
|
279
|
+
effectType: "spawn",
|
|
280
|
+
state,
|
|
281
|
+
timestamp
|
|
282
|
+
})) : void 0
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
/** Create a single runtime generation */
|
|
286
|
+
const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
|
|
287
|
+
actorId: id,
|
|
288
|
+
hooks,
|
|
289
|
+
skipFinalizer: true,
|
|
290
|
+
cellResources: {
|
|
291
|
+
stateRef,
|
|
292
|
+
stoppedRef,
|
|
293
|
+
eventQueue: currentQueue
|
|
294
|
+
},
|
|
295
|
+
lifecycle: buildLifecycle(),
|
|
296
|
+
wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
297
|
+
"effect_machine.actor.id": id,
|
|
298
|
+
"effect_machine.state.current": state._tag,
|
|
299
|
+
"effect_machine.event.type": event._tag
|
|
300
|
+
} })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
|
|
301
|
+
onChildSpawned: (childId, child) => Effect.gen(function* () {
|
|
302
|
+
childrenMap.set(childId, child);
|
|
303
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
304
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
305
|
+
childrenMap.delete(childId);
|
|
423
306
|
}));
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
307
|
+
})
|
|
308
|
+
})));
|
|
309
|
+
const supervision = options?.supervision;
|
|
310
|
+
const runtime = yield* spawnGeneration(machineWithState);
|
|
311
|
+
runtimeRef.current = runtime;
|
|
312
|
+
let supervisorFiber;
|
|
313
|
+
if (supervision !== void 0) supervisorFiber = yield* Effect.forkDaemon(Effect.gen(function* () {
|
|
314
|
+
const driver = yield* Schedule.driver(supervision.schedule);
|
|
315
|
+
let generation = 0;
|
|
316
|
+
while (true) {
|
|
317
|
+
const currentRuntime = runtimeRef.current;
|
|
318
|
+
if (currentRuntime === void 0) return;
|
|
319
|
+
const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
|
|
320
|
+
if (generationExit._tag !== "Defect") {
|
|
321
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (supervision.shouldRestart !== void 0 && !supervision.shouldRestart(generationExit)) {
|
|
325
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if ((yield* driver.next(generationExit).pipe(Effect.exit))._tag === "Failure") {
|
|
329
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
333
|
+
const freshQueue = yield* Queue.unbounded();
|
|
334
|
+
yield* Ref.set(eventQueueRef, freshQueue);
|
|
335
|
+
yield* SubscriptionRef.set(stateRef, machine.initial);
|
|
336
|
+
yield* Ref.set(stoppedRef, false);
|
|
337
|
+
childrenMap.clear();
|
|
338
|
+
runtimeRef.current = yield* spawnGeneration(machine);
|
|
339
|
+
generation++;
|
|
340
|
+
if (options?.onRestart !== void 0) yield* options.onRestart(generation, generationExit);
|
|
341
|
+
notifyListeners(listeners, machine.initial);
|
|
428
342
|
}
|
|
429
|
-
}
|
|
430
|
-
return {
|
|
431
|
-
shouldStop: false,
|
|
432
|
-
result
|
|
433
|
-
};
|
|
434
|
-
});
|
|
435
|
-
/**
|
|
436
|
-
* Run spawn effects with actor-specific inspection and tracing.
|
|
437
|
-
* Wraps the core runSpawnEffects with inspection events and spans.
|
|
438
|
-
* @internal
|
|
439
|
-
*/
|
|
440
|
-
const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
|
|
441
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
442
|
-
type: "@machine.effect",
|
|
443
|
-
actorId,
|
|
444
|
-
effectType: "spawn",
|
|
445
|
-
state,
|
|
446
|
-
timestamp
|
|
447
343
|
}));
|
|
448
|
-
yield*
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
344
|
+
else yield* Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit)), Effect.catchAllCause(() => Effect.void), Effect.fork);
|
|
345
|
+
return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
|
|
346
|
+
if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
|
|
347
|
+
const currentRuntime = runtimeRef.current;
|
|
348
|
+
if (currentRuntime !== void 0) yield* currentRuntime.stop;
|
|
349
|
+
yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
|
|
350
|
+
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
351
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
|
|
352
|
+
});
|
|
353
|
+
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
354
|
+
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
355
|
+
const error = new ActorStoppedError({ actorId });
|
|
356
|
+
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
357
|
+
pendingReplies.clear();
|
|
457
358
|
});
|
|
458
359
|
/** Notify all system event listeners (sync). */
|
|
459
360
|
const notifySystemListeners = (listeners, event) => {
|
|
@@ -474,7 +375,6 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
474
375
|
});
|
|
475
376
|
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.zipRight(PubSub.shutdown(eventPubSub)), Effect.asVoid);
|
|
476
377
|
});
|
|
477
|
-
/** Check for duplicate ID, register actor, attach scope cleanup if available */
|
|
478
378
|
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
|
|
479
379
|
if (MutableHashMap.has(actorsMap, id)) {
|
|
480
380
|
yield* actor.stop;
|
|
@@ -493,7 +393,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
493
393
|
yield* emitSystemEvent({
|
|
494
394
|
_tag: "ActorStopped",
|
|
495
395
|
id,
|
|
496
|
-
actor: actorRef
|
|
396
|
+
actor: actorRef,
|
|
397
|
+
exit: { _tag: "Stopped" }
|
|
497
398
|
});
|
|
498
399
|
MutableHashMap.remove(actorsMap, id);
|
|
499
400
|
}
|
|
@@ -501,11 +402,24 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
501
402
|
}));
|
|
502
403
|
return actor;
|
|
503
404
|
});
|
|
504
|
-
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id,
|
|
405
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
|
|
505
406
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
506
|
-
|
|
407
|
+
const materialized = materializeMachine(machine, spawnOptions?.slots);
|
|
408
|
+
let actorRef;
|
|
409
|
+
const actor = yield* createActor(id, materialized, {
|
|
410
|
+
supervision: spawnOptions?.supervision,
|
|
411
|
+
onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
|
|
412
|
+
_tag: "ActorRestarted",
|
|
413
|
+
id,
|
|
414
|
+
actor: actorRef,
|
|
415
|
+
generation,
|
|
416
|
+
exit
|
|
417
|
+
}) : Effect.void : void 0
|
|
418
|
+
});
|
|
419
|
+
actorRef = actor;
|
|
420
|
+
return yield* registerActor(id, actor);
|
|
507
421
|
});
|
|
508
|
-
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
422
|
+
const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
|
|
509
423
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
510
424
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
511
425
|
});
|
|
@@ -517,7 +431,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
517
431
|
yield* emitSystemEvent({
|
|
518
432
|
_tag: "ActorStopped",
|
|
519
433
|
id,
|
|
520
|
-
actor
|
|
434
|
+
actor,
|
|
435
|
+
exit: { _tag: "Stopped" }
|
|
521
436
|
});
|
|
522
437
|
yield* actor.stop;
|
|
523
438
|
return true;
|
|
@@ -543,8 +458,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
543
458
|
});
|
|
544
459
|
});
|
|
545
460
|
/**
|
|
461
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
462
|
+
* @internal — use Default layer for normal usage
|
|
463
|
+
*/
|
|
464
|
+
const makeSystem = make;
|
|
465
|
+
/**
|
|
546
466
|
* Default ActorSystem layer
|
|
547
467
|
*/
|
|
548
|
-
const Default = Layer.
|
|
468
|
+
const Default = Layer.effect(ActorSystem, make());
|
|
549
469
|
//#endregion
|
|
550
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
470
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stubSystem } from "../internal/utils.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ActorSystem } from "../actor.js";
|
|
2
|
+
import { replay } from "../machine.js";
|
|
4
3
|
import { createRuntime } from "../internal/runtime.js";
|
|
4
|
+
import { ActorSystem } from "../actor.js";
|
|
5
5
|
import { PersistenceAdapter } from "./persistence.js";
|
|
6
6
|
import { Effect, Option, Ref } from "effect";
|
|
7
7
|
import { Entity } from "@effect/cluster";
|
|
@@ -47,7 +47,8 @@ const EntityMachine = { layer: (entity, machine, options) => {
|
|
|
47
47
|
const versionRef = yield* Ref.make(persistCtx.initialVersion);
|
|
48
48
|
const runtime = yield* createRuntime(machineWithState, system, {
|
|
49
49
|
actorId: entityId,
|
|
50
|
-
hooks: options?.hooks
|
|
50
|
+
hooks: options?.hooks,
|
|
51
|
+
childIdPrefix: `${entityId}/`
|
|
51
52
|
});
|
|
52
53
|
if (persistCtx.adapter !== void 0) {
|
|
53
54
|
const { adapter: pAdapter, key } = persistCtx;
|
|
@@ -110,8 +111,7 @@ const hydratePersistence = (persistence, entityDef, entityId, machine, initializ
|
|
|
110
111
|
const snapshotVersion = Option.isSome(maybeSnapshot) ? maybeSnapshot.value.version : 0;
|
|
111
112
|
const events = yield* adapter.loadEvents(key, snapshotVersion);
|
|
112
113
|
if (events.length > 0) {
|
|
113
|
-
const
|
|
114
|
-
const hydratedState = yield* replay(new BuiltMachine(machine), eventValues, { from: baseState });
|
|
114
|
+
const hydratedState = yield* replay(machine, events.map((e) => e.event), { from: baseState });
|
|
115
115
|
const lastEvent = events[events.length - 1];
|
|
116
116
|
return {
|
|
117
117
|
adapter,
|