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/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, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
7
8
|
//#region src/actor.ts
|
|
8
9
|
/**
|
|
9
10
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -28,10 +29,11 @@ const notifyListeners = (listeners, state) => {
|
|
|
28
29
|
/**
|
|
29
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
|
|
@@ -126,12 +130,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
126
130
|
listeners.delete(fn);
|
|
127
131
|
};
|
|
128
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),
|
|
129
145
|
sync: {
|
|
130
146
|
send: (event) => {
|
|
131
|
-
if (!Effect.runSync(Ref.get(stoppedRef)))
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|
|
135
154
|
},
|
|
136
155
|
stop: () => Effect.runFork(stop),
|
|
137
156
|
snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
@@ -144,325 +163,251 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
144
163
|
children: childrenMap
|
|
145
164
|
};
|
|
146
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
|
+
});
|
|
147
193
|
/**
|
|
148
|
-
*
|
|
194
|
+
* Load persisted state and run onRestore hook if present.
|
|
195
|
+
* Returns the resolved initial state (loaded, restored, or fallback to machineInitial).
|
|
196
|
+
* @internal
|
|
149
197
|
*/
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
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* () {
|
|
153
210
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
else {
|
|
158
|
-
const scope = yield* Scope.make();
|
|
159
|
-
system = yield* make().pipe(Effect.provideService(Scope.Scope, scope));
|
|
160
|
-
implicitSystemScope = scope;
|
|
161
|
-
}
|
|
162
|
-
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
163
|
-
const eventQueue = yield* Queue.unbounded();
|
|
164
|
-
const stoppedRef = yield* Ref.make(false);
|
|
165
|
-
const childrenMap = /* @__PURE__ */ new Map();
|
|
166
|
-
const deferredReplyRef = { current: void 0 };
|
|
167
|
-
const selfSend = Effect.fn("effect-machine.actor.self.send")(function* (event) {
|
|
168
|
-
if (yield* Ref.get(stoppedRef)) return;
|
|
169
|
-
yield* Queue.offer(eventQueue, {
|
|
170
|
-
_tag: "send",
|
|
171
|
-
event
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
const self = {
|
|
175
|
-
send: selfSend,
|
|
176
|
-
cast: selfSend,
|
|
177
|
-
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
178
|
-
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
179
|
-
childrenMap.set(childId, child);
|
|
180
|
-
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
181
|
-
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
182
|
-
childrenMap.delete(childId);
|
|
183
|
-
}));
|
|
184
|
-
return child;
|
|
185
|
-
}),
|
|
186
|
-
reply: (value) => Effect.sync(() => {
|
|
187
|
-
const deferred = deferredReplyRef.current;
|
|
188
|
-
if (deferred !== void 0) {
|
|
189
|
-
deferredReplyRef.current = void 0;
|
|
190
|
-
Effect.runFork(Deferred.succeed(deferred, value));
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
})
|
|
211
|
+
if (Option.isSome(existingSystem)) return {
|
|
212
|
+
system: existingSystem.value,
|
|
213
|
+
implicitSystemScope: void 0
|
|
195
214
|
};
|
|
196
|
-
yield*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
initialState: initial,
|
|
201
|
-
timestamp
|
|
202
|
-
}));
|
|
203
|
-
const stateRef = yield* SubscriptionRef.make(initial);
|
|
204
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
205
|
-
const backgroundFibers = [];
|
|
206
|
-
const initEvent = { _tag: INTERNAL_INIT_EVENT };
|
|
207
|
-
const ctx = {
|
|
208
|
-
actorId: id,
|
|
209
|
-
state: initial,
|
|
210
|
-
event: initEvent,
|
|
211
|
-
self,
|
|
212
|
-
system
|
|
215
|
+
const scope = yield* Scope.make();
|
|
216
|
+
return {
|
|
217
|
+
system: yield* make().pipe(Effect.provideService(Scope.Scope, scope)),
|
|
218
|
+
implicitSystemScope: scope
|
|
213
219
|
};
|
|
214
|
-
const { effects: effectSlots } = machine._slots;
|
|
215
|
-
for (const bg of machine.backgroundEffects) {
|
|
216
|
-
const fiber = yield* Effect.forkDetach(bg.handler({
|
|
217
|
-
actorId: id,
|
|
218
|
-
state: initial,
|
|
219
|
-
event: initEvent,
|
|
220
|
-
self,
|
|
221
|
-
effects: effectSlots,
|
|
222
|
-
system
|
|
223
|
-
}).pipe(Effect.provideService(machine.Context, ctx)));
|
|
224
|
-
backgroundFibers.push(fiber);
|
|
225
|
-
}
|
|
226
|
-
const stateScopeRef = { current: yield* Scope.make() };
|
|
227
|
-
yield* runSpawnEffectsWithInspection(machine, initial, initEvent, self, stateScopeRef.current, id, inspectorValue, system);
|
|
228
|
-
if (machine.finalStates.has(initial._tag)) {
|
|
229
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
230
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
231
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
232
|
-
type: "@machine.stop",
|
|
233
|
-
actorId: id,
|
|
234
|
-
finalState: initial,
|
|
235
|
-
timestamp
|
|
236
|
-
}));
|
|
237
|
-
yield* Ref.set(stoppedRef, true);
|
|
238
|
-
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
239
|
-
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());
|
|
240
|
-
}
|
|
241
|
-
const pendingReplies = /* @__PURE__ */ new Set();
|
|
242
|
-
const transitionsPubSub = yield* PubSub.unbounded();
|
|
243
|
-
const loopFiber = yield* Effect.forkDetach(eventLoop(machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, id, inspectorValue, system, pendingReplies, transitionsPubSub, deferredReplyRef));
|
|
244
|
-
return buildActorRefCore(id, machine, stateRef, eventQueue, stoppedRef, listeners, Effect.gen(function* () {
|
|
245
|
-
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
246
|
-
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
247
|
-
type: "@machine.stop",
|
|
248
|
-
actorId: id,
|
|
249
|
-
finalState,
|
|
250
|
-
timestamp
|
|
251
|
-
}));
|
|
252
|
-
yield* Ref.set(stoppedRef, true);
|
|
253
|
-
yield* Fiber.interrupt(loopFiber);
|
|
254
|
-
yield* settlePendingReplies(pendingReplies, id);
|
|
255
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
256
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
257
|
-
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
258
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
|
|
259
|
-
});
|
|
260
|
-
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
261
|
-
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
262
|
-
const error = new ActorStoppedError({ actorId });
|
|
263
|
-
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
264
|
-
pendingReplies.clear();
|
|
265
220
|
});
|
|
266
221
|
/**
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
222
|
+
* Run the supervision loop for a supervised actor.
|
|
223
|
+
* Observes exit deferred, applies restart policy, resets cell resources on restart.
|
|
224
|
+
* @internal
|
|
270
225
|
*/
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
275
|
-
const event = queued.event;
|
|
276
|
-
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
277
|
-
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
278
|
-
postponed.push(queued);
|
|
279
|
-
if (queued._tag === "call") {
|
|
280
|
-
const postponedResult = {
|
|
281
|
-
newState: currentState,
|
|
282
|
-
previousState: currentState,
|
|
283
|
-
transitioned: false,
|
|
284
|
-
lifecycleRan: false,
|
|
285
|
-
isFinal: false,
|
|
286
|
-
hasReply: false,
|
|
287
|
-
deferReply: false,
|
|
288
|
-
reply: void 0,
|
|
289
|
-
postponed: true
|
|
290
|
-
};
|
|
291
|
-
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
292
|
-
}
|
|
293
|
-
return {
|
|
294
|
-
shouldStop: false,
|
|
295
|
-
stateChanged: false
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
299
|
-
"effect_machine.actor.id": actorId,
|
|
300
|
-
"effect_machine.state.current": currentState._tag,
|
|
301
|
-
"effect_machine.event.type": event._tag
|
|
302
|
-
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
|
|
303
|
-
switch (queued._tag) {
|
|
304
|
-
case "call":
|
|
305
|
-
yield* Deferred.succeed(queued.reply, result);
|
|
306
|
-
break;
|
|
307
|
-
case "ask":
|
|
308
|
-
if (result.hasReply) {
|
|
309
|
-
const replySchema = machine._replySchemas?.get(event._tag);
|
|
310
|
-
if (replySchema !== void 0) {
|
|
311
|
-
let decoded;
|
|
312
|
-
try {
|
|
313
|
-
decoded = Schema.decodeUnknownSync(replySchema)(result.reply);
|
|
314
|
-
} catch (decodeError) {
|
|
315
|
-
yield* Deferred.die(queued.reply, decodeError);
|
|
316
|
-
return yield* Effect.die(decodeError);
|
|
317
|
-
}
|
|
318
|
-
yield* Deferred.succeed(queued.reply, decoded);
|
|
319
|
-
} else yield* Deferred.succeed(queued.reply, result.reply);
|
|
320
|
-
} else if (result.deferReply) deferredReplyRef.current = queued.reply;
|
|
321
|
-
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
322
|
-
actorId,
|
|
323
|
-
eventTag: event._tag
|
|
324
|
-
}));
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
328
|
-
fromState: result.previousState,
|
|
329
|
-
toState: result.newState,
|
|
330
|
-
event
|
|
331
|
-
});
|
|
332
|
-
return {
|
|
333
|
-
shouldStop,
|
|
334
|
-
stateChanged: result.lifecycleRan
|
|
335
|
-
};
|
|
336
|
-
});
|
|
226
|
+
const runSupervisionLoop = (params) => Effect.gen(function* () {
|
|
227
|
+
const step = yield* Schedule.toStepWithSleep(params.supervision.schedule);
|
|
228
|
+
let generation = 0;
|
|
337
229
|
while (true) {
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
yield*
|
|
343
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
344
|
-
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);
|
|
345
235
|
return;
|
|
346
236
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
yield* Ref.set(stoppedRef, true);
|
|
355
|
-
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
356
|
-
yield* settlePendingReplies(pendingReplies, actorId);
|
|
357
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
358
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (drain.stateChanged) drainTriggered = true;
|
|
362
|
-
}
|
|
237
|
+
if (params.supervision.shouldRestart !== void 0 && !params.supervision.shouldRestart(generationExit)) {
|
|
238
|
+
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
|
|
242
|
+
yield* Deferred.succeed(params.terminalExitDeferred, generationExit);
|
|
243
|
+
return;
|
|
363
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);
|
|
364
261
|
}
|
|
365
262
|
});
|
|
366
263
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* (so their pendingReplies entry is already removed). Ask/send entries
|
|
370
|
-
* with Deferreds are settled via the pendingReplies registry.
|
|
371
|
-
*/
|
|
372
|
-
const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
|
|
373
|
-
postponed.length = 0;
|
|
374
|
-
};
|
|
375
|
-
/**
|
|
376
|
-
* Process a single event, returning true if the actor should stop.
|
|
377
|
-
* 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.
|
|
378
266
|
*/
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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,
|
|
385
282
|
timestamp
|
|
386
283
|
}));
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
})),
|
|
403
|
-
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
404
|
-
type: "@machine.error",
|
|
405
|
-
actorId,
|
|
406
|
-
phase: info.phase,
|
|
407
|
-
state: info.state,
|
|
408
|
-
event: info.event,
|
|
409
|
-
error: Cause.pretty(info.cause),
|
|
410
|
-
timestamp
|
|
411
|
-
}))
|
|
412
|
-
});
|
|
413
|
-
if (!result.transitioned) {
|
|
414
|
-
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;
|
|
415
299
|
return {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
422
|
-
notifyListeners(listeners, result.newState);
|
|
423
|
-
if (result.lifecycleRan) {
|
|
424
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
425
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
426
|
-
if (result.isFinal) {
|
|
427
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
428
|
-
type: "@machine.stop",
|
|
429
|
-
actorId,
|
|
430
|
-
finalState: result.newState,
|
|
300
|
+
onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
301
|
+
type: "@machine.event",
|
|
302
|
+
actorId: id,
|
|
303
|
+
state,
|
|
304
|
+
event,
|
|
431
305
|
timestamp
|
|
432
|
-
}))
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
result
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
+
};
|
|
442
352
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
state,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
})
|
|
465
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.forkDetach(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.forkDetach(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();
|
|
466
411
|
});
|
|
467
412
|
/** Notify all system event listeners (sync). */
|
|
468
413
|
const notifySystemListeners = (listeners, event) => {
|
|
@@ -502,7 +447,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
502
447
|
yield* emitSystemEvent({
|
|
503
448
|
_tag: "ActorStopped",
|
|
504
449
|
id,
|
|
505
|
-
actor: actorRef
|
|
450
|
+
actor: actorRef,
|
|
451
|
+
exit: { _tag: "Stopped" }
|
|
506
452
|
});
|
|
507
453
|
MutableHashMap.remove(actorsMap, id);
|
|
508
454
|
}
|
|
@@ -510,11 +456,25 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
510
456
|
}));
|
|
511
457
|
return actor;
|
|
512
458
|
});
|
|
513
|
-
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id,
|
|
459
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
|
|
514
460
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
515
|
-
|
|
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);
|
|
516
476
|
});
|
|
517
|
-
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
477
|
+
const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
|
|
518
478
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
519
479
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
520
480
|
});
|
|
@@ -526,7 +486,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
526
486
|
yield* emitSystemEvent({
|
|
527
487
|
_tag: "ActorStopped",
|
|
528
488
|
id,
|
|
529
|
-
actor
|
|
489
|
+
actor,
|
|
490
|
+
exit: { _tag: "Stopped" }
|
|
530
491
|
});
|
|
531
492
|
yield* actor.stop;
|
|
532
493
|
return true;
|