effect-machine 0.12.0 → 0.14.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 +133 -324
- package/dist/actor.d.ts +46 -28
- package/dist/actor.js +276 -315
- package/dist/cluster/entity-machine.d.ts +1 -1
- package/dist/cluster/entity-machine.js +20 -9
- package/dist/cluster/to-entity.d.ts +3 -3
- package/dist/errors.d.ts +24 -20
- package/dist/errors.js +10 -6
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/internal/runtime.d.ts +82 -7
- package/dist/internal/runtime.js +162 -58
- package/dist/internal/transition.d.ts +12 -11
- package/dist/internal/transition.js +12 -14
- package/dist/machine.d.ts +148 -140
- package/dist/machine.js +141 -155
- package/dist/schema.d.ts +14 -0
- package/dist/schema.js +10 -1
- package/dist/slot.d.ts +112 -86
- package/dist/slot.js +92 -59
- package/dist/supervision.d.ts +97 -0
- package/dist/supervision.js +42 -0
- package/dist/testing.d.ts +21 -12
- package/dist/testing.js +23 -26
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +53 -30
- package/v3/dist/actor.js +286 -311
- package/v3/dist/cluster/entity-machine.d.ts +1 -1
- package/v3/dist/cluster/entity-machine.js +5 -5
- package/v3/dist/cluster/to-entity.d.ts +1 -1
- package/v3/dist/errors.d.ts +14 -10
- package/v3/dist/errors.js +11 -7
- package/v3/dist/index.d.ts +6 -5
- package/v3/dist/index.js +3 -2
- package/v3/dist/inspection.d.ts +3 -22
- package/v3/dist/inspection.js +1 -15
- package/v3/dist/internal/brands.d.ts +4 -8
- package/v3/dist/internal/inspection.js +1 -1
- package/v3/dist/internal/runtime.d.ts +87 -10
- package/v3/dist/internal/runtime.js +177 -61
- package/v3/dist/internal/transition.d.ts +13 -12
- package/v3/dist/internal/transition.js +14 -16
- package/v3/dist/internal/utils.js +5 -1
- package/v3/dist/machine.d.ts +158 -143
- package/v3/dist/machine.js +148 -155
- package/v3/dist/schema.d.ts +25 -11
- package/v3/dist/schema.js +18 -5
- package/v3/dist/slot.d.ts +112 -86
- package/v3/dist/slot.js +92 -59
- package/v3/dist/supervision.d.ts +97 -0
- package/v3/dist/supervision.js +42 -0
- package/v3/dist/testing.d.ts +21 -12
- package/v3/dist/testing.js +23 -24
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.
|
|
@@ -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)),
|
|
@@ -146,314 +163,251 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
146
163
|
children: childrenMap
|
|
147
164
|
};
|
|
148
165
|
};
|
|
166
|
+
/** Build ProcessEventHooks from an inspector */
|
|
167
|
+
const buildInspectionHooks = (actorId, inspector) => ({
|
|
168
|
+
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
169
|
+
type: "@machine.effect",
|
|
170
|
+
actorId,
|
|
171
|
+
effectType: "spawn",
|
|
172
|
+
state,
|
|
173
|
+
timestamp
|
|
174
|
+
})),
|
|
175
|
+
onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
176
|
+
type: "@machine.transition",
|
|
177
|
+
actorId,
|
|
178
|
+
fromState: from,
|
|
179
|
+
toState: to,
|
|
180
|
+
event: ev,
|
|
181
|
+
timestamp
|
|
182
|
+
})),
|
|
183
|
+
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
184
|
+
type: "@machine.error",
|
|
185
|
+
actorId,
|
|
186
|
+
phase: info.phase,
|
|
187
|
+
state: info.state,
|
|
188
|
+
event: info.event,
|
|
189
|
+
error: Cause.pretty(info.cause),
|
|
190
|
+
timestamp
|
|
191
|
+
}))
|
|
192
|
+
});
|
|
149
193
|
/**
|
|
150
|
-
*
|
|
194
|
+
* Load persisted state and run onRestore hook if present.
|
|
195
|
+
* Returns the resolved initial state (loaded, restored, or fallback to machineInitial).
|
|
196
|
+
* @internal
|
|
151
197
|
*/
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
|
|
198
|
+
const loadAndRestore = (persist, machineInitial) => Effect.gen(function* () {
|
|
199
|
+
const loaded = yield* persist.load();
|
|
200
|
+
if (Option.isNone(loaded)) return machineInitial;
|
|
201
|
+
if (persist.onRestore === void 0) return loaded.value;
|
|
202
|
+
const restored = yield* persist.onRestore(loaded.value, { initial: machineInitial });
|
|
203
|
+
return Option.getOrElse(restored, () => machineInitial);
|
|
204
|
+
});
|
|
205
|
+
/**
|
|
206
|
+
* Resolve actor system from context, creating an implicit one if none exists.
|
|
207
|
+
* @internal
|
|
208
|
+
*/
|
|
209
|
+
const resolveActorSystem = Effect.fn("effect-machine.resolveActorSystem")(function* () {
|
|
155
210
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
else {
|
|
160
|
-
const scope = yield* Scope.make();
|
|
161
|
-
system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
|
|
162
|
-
implicitSystemScope = scope;
|
|
163
|
-
}
|
|
164
|
-
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
165
|
-
const eventQueue = yield* Queue.unbounded();
|
|
166
|
-
const stoppedRef = yield* Ref.make(false);
|
|
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
|
-
});
|
|
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
|
-
})
|
|
211
|
+
if (Option.isSome(existingSystem)) return {
|
|
212
|
+
system: existingSystem.value,
|
|
213
|
+
implicitSystemScope: void 0
|
|
187
214
|
};
|
|
188
|
-
yield*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
initialState: initial,
|
|
193
|
-
timestamp
|
|
194
|
-
}));
|
|
195
|
-
const stateRef = yield* SubscriptionRef.make(initial);
|
|
196
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
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
|
|
215
|
+
const scope = yield* Scope.make();
|
|
216
|
+
return {
|
|
217
|
+
system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
|
|
218
|
+
implicitSystemScope: scope
|
|
205
219
|
};
|
|
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
220
|
});
|
|
258
221
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
222
|
+
* Run the supervision loop for a supervised actor.
|
|
223
|
+
* Observes exit deferred, applies restart policy, resets cell resources on restart.
|
|
224
|
+
* @internal
|
|
262
225
|
*/
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
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
|
-
});
|
|
226
|
+
const runSupervisionLoop = (params) => Effect.gen(function* () {
|
|
227
|
+
const driver = yield* Schedule.driver(params.supervision.schedule);
|
|
228
|
+
let generation = 0;
|
|
328
229
|
while (true) {
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
yield*
|
|
334
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
335
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
230
|
+
const currentRuntime = params.runtimeRef.current;
|
|
231
|
+
if (currentRuntime === void 0) return;
|
|
232
|
+
const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
|
|
233
|
+
if (generationExit._tag !== "Defect") {
|
|
234
|
+
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
336
235
|
return;
|
|
337
236
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
}
|
|
237
|
+
if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
|
|
238
|
+
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
239
|
+
return;
|
|
354
240
|
}
|
|
241
|
+
if ((yield* driver.next(generationExit).pipe(Effect.exit))._tag === "Failure") {
|
|
242
|
+
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const restartState = params.persist !== void 0 ? yield* loadAndRestore(params.persist, params.machine.initial) : params.machine.initial;
|
|
246
|
+
yield* settlePendingReplies(params.pendingReplies, params.id);
|
|
247
|
+
const freshQueue = yield* Queue.unbounded();
|
|
248
|
+
yield* Ref.set(params.eventQueueRef, freshQueue);
|
|
249
|
+
yield* SubscriptionRef.set(params.stateRef, restartState);
|
|
250
|
+
yield* Ref.set(params.stoppedRef, false);
|
|
251
|
+
params.childrenMap.clear();
|
|
252
|
+
const machineForRestart = restartState !== params.machine.initial ? Object.create(params.machine, { initial: {
|
|
253
|
+
value: restartState,
|
|
254
|
+
enumerable: true
|
|
255
|
+
} }) : params.machine;
|
|
256
|
+
const newRuntime = yield* params.spawnGeneration(machineForRestart);
|
|
257
|
+
params.runtimeRef.current = newRuntime;
|
|
258
|
+
generation++;
|
|
259
|
+
if (params.onRestart !== void 0) yield* params.onRestart(generation, generationExit);
|
|
260
|
+
notifyListeners(params.listeners, restartState);
|
|
355
261
|
}
|
|
356
262
|
});
|
|
357
263
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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).
|
|
264
|
+
* Create and start an actor for a machine.
|
|
265
|
+
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
369
266
|
*/
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
267
|
+
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
268
|
+
const persist = options?.persist;
|
|
269
|
+
const initial = options?.initialState ?? (persist !== void 0 ? yield* loadAndRestore(persist, machine.initial) : machine.initial);
|
|
270
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
271
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
272
|
+
const { system, implicitSystemScope } = yield* resolveActorSystem();
|
|
273
|
+
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
274
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
275
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
276
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
277
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
278
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
279
|
+
type: "@machine.spawn",
|
|
280
|
+
actorId: id,
|
|
281
|
+
initialState: initial,
|
|
376
282
|
timestamp
|
|
377
283
|
}));
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
})),
|
|
394
|
-
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
395
|
-
type: "@machine.error",
|
|
396
|
-
actorId,
|
|
397
|
-
phase: info.phase,
|
|
398
|
-
state: info.state,
|
|
399
|
-
event: info.event,
|
|
400
|
-
error: Cause.pretty(info.cause),
|
|
401
|
-
timestamp
|
|
402
|
-
}))
|
|
403
|
-
});
|
|
404
|
-
if (!result.transitioned) {
|
|
405
|
-
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
284
|
+
const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
|
|
285
|
+
const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
|
|
286
|
+
value: initial,
|
|
287
|
+
enumerable: true
|
|
288
|
+
} }) : machine;
|
|
289
|
+
const stateRef = yield* SubscriptionRef.make(initial);
|
|
290
|
+
const stoppedRef = yield* Ref.make(false);
|
|
291
|
+
const initialQueue = yield* Queue.unbounded();
|
|
292
|
+
const eventQueueRef = yield* Ref.make(initialQueue);
|
|
293
|
+
const terminalExitDeferred = yield* Deferred.make();
|
|
294
|
+
let stopEmitted = false;
|
|
295
|
+
const runtimeRef = { current: void 0 };
|
|
296
|
+
/** Build lifecycle hooks for a generation */
|
|
297
|
+
const buildLifecycle = () => {
|
|
298
|
+
stopEmitted = false;
|
|
406
299
|
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,
|
|
300
|
+
onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
301
|
+
type: "@machine.event",
|
|
302
|
+
actorId: id,
|
|
303
|
+
state,
|
|
304
|
+
event,
|
|
422
305
|
timestamp
|
|
423
|
-
}))
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
result
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
306
|
+
})) : void 0,
|
|
307
|
+
onStateChange: (result, _event) => Effect.gen(function* () {
|
|
308
|
+
notifyListeners(listeners, result.newState);
|
|
309
|
+
if (persist !== void 0 && result.transitioned) {
|
|
310
|
+
if (persist.shouldSave === void 0 || persist.shouldSave(result.newState, result.previousState)) yield* persist.save(result.newState);
|
|
311
|
+
}
|
|
312
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
313
|
+
if (result.lifecycleRan) {
|
|
314
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
315
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
316
|
+
}
|
|
317
|
+
}),
|
|
318
|
+
onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
|
|
319
|
+
fromState: result.previousState,
|
|
320
|
+
toState: result.newState,
|
|
321
|
+
event
|
|
322
|
+
}).pipe(Effect.asVoid) : Effect.void,
|
|
323
|
+
onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
|
|
324
|
+
stopEmitted = true;
|
|
325
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
326
|
+
type: "@machine.stop",
|
|
327
|
+
actorId: id,
|
|
328
|
+
finalState: state,
|
|
329
|
+
timestamp
|
|
330
|
+
}));
|
|
331
|
+
}) : void 0,
|
|
332
|
+
onShutdown: () => Effect.gen(function* () {
|
|
333
|
+
if (!stopEmitted) {
|
|
334
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
335
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
336
|
+
type: "@machine.stop",
|
|
337
|
+
actorId: id,
|
|
338
|
+
finalState,
|
|
339
|
+
timestamp
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
343
|
+
}),
|
|
344
|
+
onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
345
|
+
type: "@machine.effect",
|
|
346
|
+
actorId: id,
|
|
347
|
+
effectType: "spawn",
|
|
348
|
+
state,
|
|
349
|
+
timestamp
|
|
350
|
+
})) : void 0
|
|
351
|
+
};
|
|
433
352
|
};
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
state,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
353
|
+
/** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
|
|
354
|
+
const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
|
|
355
|
+
actorId: id,
|
|
356
|
+
hooks,
|
|
357
|
+
skipFinalizer: true,
|
|
358
|
+
cellResources: {
|
|
359
|
+
stateRef,
|
|
360
|
+
stoppedRef,
|
|
361
|
+
eventQueue: currentQueue
|
|
362
|
+
},
|
|
363
|
+
lifecycle: buildLifecycle(),
|
|
364
|
+
wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
365
|
+
"effect_machine.actor.id": id,
|
|
366
|
+
"effect_machine.state.current": state._tag,
|
|
367
|
+
"effect_machine.event.type": event._tag
|
|
368
|
+
} })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
|
|
369
|
+
onChildSpawned: (childId, child) => Effect.gen(function* () {
|
|
370
|
+
childrenMap.set(childId, child);
|
|
371
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
372
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
373
|
+
childrenMap.delete(childId);
|
|
374
|
+
}));
|
|
375
|
+
})
|
|
456
376
|
})));
|
|
377
|
+
const runtime = yield* spawnGeneration(machineWithState);
|
|
378
|
+
runtimeRef.current = runtime;
|
|
379
|
+
const supervision = options?.supervision;
|
|
380
|
+
let supervisorFiber;
|
|
381
|
+
if (supervision !== void 0) supervisorFiber = yield* Effect.forkDaemon(runSupervisionLoop({
|
|
382
|
+
supervision,
|
|
383
|
+
machine,
|
|
384
|
+
id,
|
|
385
|
+
runtimeRef,
|
|
386
|
+
terminalExitDeferred,
|
|
387
|
+
pendingReplies,
|
|
388
|
+
eventQueueRef,
|
|
389
|
+
stateRef,
|
|
390
|
+
stoppedRef,
|
|
391
|
+
childrenMap,
|
|
392
|
+
listeners,
|
|
393
|
+
spawnGeneration,
|
|
394
|
+
persist,
|
|
395
|
+
onRestart: options?.onRestart
|
|
396
|
+
}));
|
|
397
|
+
else yield* Effect.forkDaemon(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
|
|
398
|
+
return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
|
|
399
|
+
if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
|
|
400
|
+
const currentRuntime = runtimeRef.current;
|
|
401
|
+
if (currentRuntime !== void 0) yield* currentRuntime.stop;
|
|
402
|
+
yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
|
|
403
|
+
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
404
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
|
|
405
|
+
});
|
|
406
|
+
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
407
|
+
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
408
|
+
const error = new ActorStoppedError({ actorId });
|
|
409
|
+
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
410
|
+
pendingReplies.clear();
|
|
457
411
|
});
|
|
458
412
|
/** Notify all system event listeners (sync). */
|
|
459
413
|
const notifySystemListeners = (listeners, event) => {
|
|
@@ -466,13 +420,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
466
420
|
const withSpawnGate = (yield* Effect.makeSemaphore(1)).withPermits(1);
|
|
467
421
|
const eventPubSub = yield* PubSub.unbounded();
|
|
468
422
|
const eventListeners = /* @__PURE__ */ new Set();
|
|
469
|
-
const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.
|
|
423
|
+
const emitSystemEvent = (event) => Effect.sync(() => notifySystemListeners(eventListeners, event)).pipe(Effect.andThen(PubSub.publish(eventPubSub, event)), Effect.catchAllCause(() => Effect.void), Effect.asVoid);
|
|
470
424
|
yield* Effect.addFinalizer(() => {
|
|
471
425
|
const stops = [];
|
|
472
426
|
MutableHashMap.forEach(actorsMap, (actor) => {
|
|
473
427
|
stops.push(actor.stop);
|
|
474
428
|
});
|
|
475
|
-
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.
|
|
429
|
+
return Effect.all(stops, { concurrency: "unbounded" }).pipe(Effect.andThen(PubSub.shutdown(eventPubSub)), Effect.asVoid);
|
|
476
430
|
});
|
|
477
431
|
/** Check for duplicate ID, register actor, attach scope cleanup if available */
|
|
478
432
|
const registerActor = Effect.fn("effect-machine.actorSystem.register")(function* (id, actor) {
|
|
@@ -493,7 +447,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
493
447
|
yield* emitSystemEvent({
|
|
494
448
|
_tag: "ActorStopped",
|
|
495
449
|
id,
|
|
496
|
-
actor: actorRef
|
|
450
|
+
actor: actorRef,
|
|
451
|
+
exit: { _tag: "Stopped" }
|
|
497
452
|
});
|
|
498
453
|
MutableHashMap.remove(actorsMap, id);
|
|
499
454
|
}
|
|
@@ -501,11 +456,25 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
501
456
|
}));
|
|
502
457
|
return actor;
|
|
503
458
|
});
|
|
504
|
-
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id,
|
|
459
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
|
|
505
460
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
506
|
-
|
|
461
|
+
const materialized = spawnOptions?.slots !== void 0 ? materializeMachine(machine, spawnOptions.slots) : machine;
|
|
462
|
+
let actorRef;
|
|
463
|
+
const actor = yield* createActor(id, materialized, {
|
|
464
|
+
supervision: spawnOptions?.supervision,
|
|
465
|
+
persist: spawnOptions?.persist,
|
|
466
|
+
onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
|
|
467
|
+
_tag: "ActorRestarted",
|
|
468
|
+
id,
|
|
469
|
+
actor: actorRef,
|
|
470
|
+
generation,
|
|
471
|
+
exit
|
|
472
|
+
}) : Effect.void : void 0
|
|
473
|
+
});
|
|
474
|
+
actorRef = actor;
|
|
475
|
+
return yield* registerActor(id, actor);
|
|
507
476
|
});
|
|
508
|
-
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
477
|
+
const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
|
|
509
478
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
510
479
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
511
480
|
});
|
|
@@ -517,7 +486,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
517
486
|
yield* emitSystemEvent({
|
|
518
487
|
_tag: "ActorStopped",
|
|
519
488
|
id,
|
|
520
|
-
actor
|
|
489
|
+
actor,
|
|
490
|
+
exit: { _tag: "Stopped" }
|
|
521
491
|
});
|
|
522
492
|
yield* actor.stop;
|
|
523
493
|
return true;
|
|
@@ -543,8 +513,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
543
513
|
});
|
|
544
514
|
});
|
|
545
515
|
/**
|
|
516
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
517
|
+
* @internal — use Default layer for normal usage
|
|
518
|
+
*/
|
|
519
|
+
const makeSystem = make;
|
|
520
|
+
/**
|
|
546
521
|
* Default ActorSystem layer
|
|
547
522
|
*/
|
|
548
523
|
const Default = Layer.scoped(ActorSystem, make());
|
|
549
524
|
//#endregion
|
|
550
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
525
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|