effect-machine 0.11.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 +52 -31
- package/dist/actor.js +218 -283
- package/dist/cluster/adapters/in-memory.d.ts +28 -0
- package/dist/cluster/adapters/in-memory.js +79 -0
- package/dist/cluster/entity-actor-ref.d.ts +56 -0
- package/dist/cluster/entity-actor-ref.js +33 -0
- package/dist/cluster/entity-machine.d.ts +31 -49
- package/dist/cluster/entity-machine.js +178 -52
- package/dist/cluster/index.d.ts +5 -2
- package/dist/cluster/index.js +4 -1
- package/dist/cluster/persistence.d.ts +49 -0
- package/dist/cluster/persistence.js +18 -0
- package/dist/cluster/to-entity.d.ts +9 -3
- package/dist/cluster/to-entity.js +16 -4
- package/dist/errors.d.ts +25 -17
- package/dist/errors.js +10 -5
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/internal/brands.d.ts +14 -1
- package/dist/internal/runtime.d.ts +142 -0
- package/dist/internal/runtime.js +357 -0
- package/dist/internal/transition.d.ts +10 -4
- package/dist/internal/transition.js +24 -12
- package/dist/internal/utils.d.ts +42 -6
- package/dist/internal/utils.js +27 -1
- package/dist/machine.d.ts +89 -55
- package/dist/machine.js +80 -68
- package/dist/schema.d.ts +35 -34
- package/dist/schema.js +33 -4
- 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 -23
- package/package.json +7 -7
- package/v3/dist/actor.d.ts +54 -37
- package/v3/dist/actor.js +209 -277
- package/v3/dist/cluster/adapters/in-memory.d.ts +15 -0
- package/v3/dist/cluster/adapters/in-memory.js +62 -0
- package/v3/dist/cluster/entity-actor-ref.d.ts +49 -0
- package/v3/dist/cluster/entity-actor-ref.js +19 -0
- package/v3/dist/cluster/entity-machine.d.ts +34 -49
- package/v3/dist/cluster/entity-machine.js +134 -50
- package/v3/dist/cluster/index.d.ts +5 -2
- package/v3/dist/cluster/index.js +4 -1
- package/v3/dist/cluster/persistence.d.ts +48 -0
- package/v3/dist/cluster/persistence.js +14 -0
- package/v3/dist/cluster/to-entity.d.ts +5 -2
- package/v3/dist/cluster/to-entity.js +12 -4
- package/v3/dist/errors.d.ts +18 -8
- package/v3/dist/errors.js +9 -4
- package/v3/dist/index.d.ts +6 -4
- package/v3/dist/index.js +3 -2
- package/v3/dist/internal/brands.d.ts +15 -1
- package/v3/dist/internal/runtime.d.ts +142 -0
- package/v3/dist/internal/runtime.js +335 -0
- package/v3/dist/internal/transition.d.ts +10 -4
- package/v3/dist/internal/transition.js +23 -11
- package/v3/dist/internal/utils.d.ts +42 -6
- package/v3/dist/internal/utils.js +27 -1
- package/v3/dist/machine.d.ts +35 -47
- package/v3/dist/machine.js +62 -64
- package/v3/dist/schema.d.ts +35 -34
- package/v3/dist/schema.js +29 -3
- 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/dist/actor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Inspector } from "./inspection.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ActorStoppedError, DuplicateActorError, NoReplyError } 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 { ActorStoppedError, DuplicateActorError } from "./errors.js";
|
|
5
|
+
import { createRuntime } from "./internal/runtime.js";
|
|
6
|
+
import { Cause, Deferred, Effect, Exit, Fiber, Layer, MutableHashMap, Option, PubSub, Queue, Ref, Schedule, Scope, Semaphore, ServiceMap, Stream, SubscriptionRef } from "effect";
|
|
7
7
|
//#region src/actor.ts
|
|
8
8
|
/**
|
|
9
9
|
* Actor system: spawning, lifecycle, and event processing.
|
|
@@ -28,10 +28,11 @@ const notifyListeners = (listeners, state) => {
|
|
|
28
28
|
/**
|
|
29
29
|
* Build core ActorRef methods.
|
|
30
30
|
*/
|
|
31
|
-
const buildActorRefCore = (id, machine, stateRef,
|
|
31
|
+
const buildActorRefCore = (id, machine, stateRef, eventQueueRef, stoppedRef, listeners, stop, system, childrenMap, pendingReplies, transitionsPubSub, exitDeferred) => {
|
|
32
32
|
const send = Effect.fn("effect-machine.actor.send")(function* (event) {
|
|
33
33
|
if (yield* Ref.get(stoppedRef)) return;
|
|
34
|
-
yield*
|
|
34
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
35
|
+
yield* Queue.offer(q, {
|
|
35
36
|
_tag: "send",
|
|
36
37
|
event
|
|
37
38
|
});
|
|
@@ -49,7 +50,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
49
50
|
}
|
|
50
51
|
const reply = yield* Deferred.make();
|
|
51
52
|
pendingReplies.add(reply);
|
|
52
|
-
yield*
|
|
53
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
54
|
+
yield* Queue.offer(q, {
|
|
53
55
|
_tag: "call",
|
|
54
56
|
event,
|
|
55
57
|
reply
|
|
@@ -66,7 +68,8 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
66
68
|
if (yield* Ref.get(stoppedRef)) return yield* new ActorStoppedError({ actorId: id });
|
|
67
69
|
const reply = yield* Deferred.make();
|
|
68
70
|
pendingReplies.add(reply);
|
|
69
|
-
yield*
|
|
71
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
72
|
+
yield* Queue.offer(q, {
|
|
70
73
|
_tag: "ask",
|
|
71
74
|
event,
|
|
72
75
|
reply
|
|
@@ -126,12 +129,27 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
126
129
|
listeners.delete(fn);
|
|
127
130
|
};
|
|
128
131
|
},
|
|
132
|
+
awaitExit: Deferred.await(exitDeferred),
|
|
133
|
+
watch: (other) => other.awaitExit,
|
|
134
|
+
drain: Effect.gen(function* () {
|
|
135
|
+
if (yield* Ref.get(stoppedRef)) return;
|
|
136
|
+
const q = yield* Ref.get(eventQueueRef);
|
|
137
|
+
const done = yield* Deferred.make();
|
|
138
|
+
yield* Queue.offer(q, {
|
|
139
|
+
_tag: "drain",
|
|
140
|
+
done
|
|
141
|
+
});
|
|
142
|
+
yield* Deferred.await(done);
|
|
143
|
+
}).pipe(Effect.asVoid),
|
|
129
144
|
sync: {
|
|
130
145
|
send: (event) => {
|
|
131
|
-
if (!Effect.runSync(Ref.get(stoppedRef)))
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
if (!Effect.runSync(Ref.get(stoppedRef))) {
|
|
147
|
+
const q = Effect.runSync(Ref.get(eventQueueRef));
|
|
148
|
+
Effect.runSync(Queue.offer(q, {
|
|
149
|
+
_tag: "send",
|
|
150
|
+
event
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
135
153
|
},
|
|
136
154
|
stop: () => Effect.runFork(stop),
|
|
137
155
|
snapshot: () => Effect.runSync(SubscriptionRef.get(stateRef)),
|
|
@@ -144,12 +162,41 @@ const buildActorRefCore = (id, machine, stateRef, eventQueue, stoppedRef, listen
|
|
|
144
162
|
children: childrenMap
|
|
145
163
|
};
|
|
146
164
|
};
|
|
165
|
+
/** Build ProcessEventHooks from an inspector */
|
|
166
|
+
const buildInspectionHooks = (actorId, inspector) => ({
|
|
167
|
+
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
168
|
+
type: "@machine.effect",
|
|
169
|
+
actorId,
|
|
170
|
+
effectType: "spawn",
|
|
171
|
+
state,
|
|
172
|
+
timestamp
|
|
173
|
+
})),
|
|
174
|
+
onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
175
|
+
type: "@machine.transition",
|
|
176
|
+
actorId,
|
|
177
|
+
fromState: from,
|
|
178
|
+
toState: to,
|
|
179
|
+
event: ev,
|
|
180
|
+
timestamp
|
|
181
|
+
})),
|
|
182
|
+
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
183
|
+
type: "@machine.error",
|
|
184
|
+
actorId,
|
|
185
|
+
phase: info.phase,
|
|
186
|
+
state: info.state,
|
|
187
|
+
event: info.event,
|
|
188
|
+
error: Cause.pretty(info.cause),
|
|
189
|
+
timestamp
|
|
190
|
+
}))
|
|
191
|
+
});
|
|
147
192
|
/**
|
|
148
|
-
* Create and start an actor for a machine
|
|
193
|
+
* Create and start an actor for a machine.
|
|
194
|
+
* Delegates to the shared runtime kernel with actor-specific lifecycle hooks.
|
|
149
195
|
*/
|
|
150
196
|
const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machine, options) {
|
|
151
197
|
const initial = options?.initialState ?? machine.initial;
|
|
152
198
|
yield* Effect.annotateCurrentSpan("effect_machine.actor.id", id);
|
|
199
|
+
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
153
200
|
const existingSystem = yield* Effect.serviceOption(ActorSystem);
|
|
154
201
|
let system;
|
|
155
202
|
let implicitSystemScope;
|
|
@@ -160,92 +207,149 @@ const createActor = Effect.fn("effect-machine.actor.spawn")(function* (id, machi
|
|
|
160
207
|
implicitSystemScope = scope;
|
|
161
208
|
}
|
|
162
209
|
const inspectorValue = Option.getOrUndefined(yield* Effect.serviceOption(Inspector));
|
|
163
|
-
const eventQueue = yield* Queue.unbounded();
|
|
164
|
-
const stoppedRef = yield* Ref.make(false);
|
|
165
210
|
const childrenMap = /* @__PURE__ */ new Map();
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
_tag: "send",
|
|
170
|
-
event
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
const self = {
|
|
174
|
-
send: selfSend,
|
|
175
|
-
cast: selfSend,
|
|
176
|
-
spawn: (childId, childMachine) => Effect.gen(function* () {
|
|
177
|
-
const child = yield* system.spawn(childId, childMachine).pipe(Effect.provideService(ActorSystem, system));
|
|
178
|
-
childrenMap.set(childId, child);
|
|
179
|
-
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
180
|
-
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
181
|
-
childrenMap.delete(childId);
|
|
182
|
-
}));
|
|
183
|
-
return child;
|
|
184
|
-
})
|
|
185
|
-
};
|
|
186
|
-
yield* Effect.annotateCurrentSpan("effect_machine.actor.initial_state", initial._tag);
|
|
211
|
+
const pendingReplies = /* @__PURE__ */ new Set();
|
|
212
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
213
|
+
const transitionsPubSub = yield* PubSub.unbounded();
|
|
187
214
|
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
188
215
|
type: "@machine.spawn",
|
|
189
216
|
actorId: id,
|
|
190
217
|
initialState: initial,
|
|
191
218
|
timestamp
|
|
192
219
|
}));
|
|
220
|
+
const hooks = inspectorValue !== void 0 ? buildInspectionHooks(id, inspectorValue) : void 0;
|
|
221
|
+
const machineWithState = initial !== machine.initial ? Object.create(machine, { initial: {
|
|
222
|
+
value: initial,
|
|
223
|
+
enumerable: true
|
|
224
|
+
} }) : machine;
|
|
193
225
|
const stateRef = yield* SubscriptionRef.make(initial);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
226
|
+
const stoppedRef = yield* Ref.make(false);
|
|
227
|
+
const initialQueue = yield* Queue.unbounded();
|
|
228
|
+
const eventQueueRef = yield* Ref.make(initialQueue);
|
|
229
|
+
const terminalExitDeferred = yield* Deferred.make();
|
|
230
|
+
let stopEmitted = false;
|
|
231
|
+
const runtimeRef = { current: void 0 };
|
|
232
|
+
/** Build lifecycle hooks for a generation */
|
|
233
|
+
const buildLifecycle = () => {
|
|
234
|
+
stopEmitted = false;
|
|
235
|
+
return {
|
|
236
|
+
onEvent: inspectorValue !== void 0 ? (state, event) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
237
|
+
type: "@machine.event",
|
|
238
|
+
actorId: id,
|
|
239
|
+
state,
|
|
240
|
+
event,
|
|
241
|
+
timestamp
|
|
242
|
+
})) : void 0,
|
|
243
|
+
onStateChange: (result, _event) => Effect.gen(function* () {
|
|
244
|
+
notifyListeners(listeners, result.newState);
|
|
245
|
+
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
246
|
+
if (result.lifecycleRan) {
|
|
247
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
248
|
+
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
249
|
+
}
|
|
250
|
+
}),
|
|
251
|
+
onProcessed: (result, event) => result.transitioned ? PubSub.publish(transitionsPubSub, {
|
|
252
|
+
fromState: result.previousState,
|
|
253
|
+
toState: result.newState,
|
|
254
|
+
event
|
|
255
|
+
}).pipe(Effect.asVoid) : Effect.void,
|
|
256
|
+
onFinal: inspectorValue !== void 0 ? (state) => Effect.gen(function* () {
|
|
257
|
+
stopEmitted = true;
|
|
258
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
259
|
+
type: "@machine.stop",
|
|
260
|
+
actorId: id,
|
|
261
|
+
finalState: state,
|
|
262
|
+
timestamp
|
|
263
|
+
}));
|
|
264
|
+
}) : void 0,
|
|
265
|
+
onShutdown: () => Effect.gen(function* () {
|
|
266
|
+
if (!stopEmitted) {
|
|
267
|
+
const finalState = yield* SubscriptionRef.get(stateRef);
|
|
268
|
+
yield* emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
269
|
+
type: "@machine.stop",
|
|
270
|
+
actorId: id,
|
|
271
|
+
finalState,
|
|
272
|
+
timestamp
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
276
|
+
}),
|
|
277
|
+
onInitialSpawnEffects: inspectorValue !== void 0 ? (state) => emitWithTimestamp(inspectorValue, (timestamp) => ({
|
|
278
|
+
type: "@machine.effect",
|
|
279
|
+
actorId: id,
|
|
280
|
+
effectType: "spawn",
|
|
281
|
+
state,
|
|
282
|
+
timestamp
|
|
283
|
+
})) : void 0
|
|
284
|
+
};
|
|
203
285
|
};
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
286
|
+
/** Create a single runtime generation. machineForGen is machineWithState for initial, machine for restarts. */
|
|
287
|
+
const spawnGeneration = (machineForGen) => Ref.get(eventQueueRef).pipe(Effect.flatMap((currentQueue) => createRuntime(machineForGen, system, {
|
|
288
|
+
actorId: id,
|
|
289
|
+
hooks,
|
|
290
|
+
skipFinalizer: true,
|
|
291
|
+
cellResources: {
|
|
292
|
+
stateRef,
|
|
293
|
+
stoppedRef,
|
|
294
|
+
eventQueue: currentQueue
|
|
295
|
+
},
|
|
296
|
+
lifecycle: buildLifecycle(),
|
|
297
|
+
wrapProcess: (state, event, inner) => Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
298
|
+
"effect_machine.actor.id": id,
|
|
299
|
+
"effect_machine.state.current": state._tag,
|
|
300
|
+
"effect_machine.event.type": event._tag
|
|
301
|
+
} })(inner.pipe(Effect.tap((r) => Effect.annotateCurrentSpan("effect_machine.transition.matched", r.result.transitioned)))),
|
|
302
|
+
onChildSpawned: (childId, child) => Effect.gen(function* () {
|
|
303
|
+
childrenMap.set(childId, child);
|
|
304
|
+
const maybeScope = yield* Effect.serviceOption(Scope.Scope);
|
|
305
|
+
if (Option.isSome(maybeScope)) yield* Scope.addFinalizer(maybeScope.value, Effect.sync(() => {
|
|
306
|
+
childrenMap.delete(childId);
|
|
307
|
+
}));
|
|
308
|
+
})
|
|
309
|
+
})));
|
|
310
|
+
const runtime = yield* spawnGeneration(machineWithState);
|
|
311
|
+
runtimeRef.current = runtime;
|
|
312
|
+
const supervision = options?.supervision;
|
|
313
|
+
let supervisorFiber;
|
|
314
|
+
if (supervision !== void 0) supervisorFiber = yield* Effect.forkDetach(Effect.gen(function* () {
|
|
315
|
+
const step = yield* Schedule.toStepWithSleep(supervision.schedule);
|
|
316
|
+
let generation = 0;
|
|
317
|
+
while (true) {
|
|
318
|
+
const currentRuntime = runtimeRef.current;
|
|
319
|
+
if (currentRuntime === void 0) return;
|
|
320
|
+
const generationExit = yield* Deferred.await(currentRuntime.exitDeferred);
|
|
321
|
+
if (generationExit._tag !== "Defect") {
|
|
322
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (supervision.shouldRestart !== void 0 && !supervision.shouldRestart(generationExit)) {
|
|
326
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if ((yield* step(generationExit).pipe(Effect.exit))._tag === "Failure") {
|
|
330
|
+
yield* Deferred.succeed(terminalExitDeferred, generationExit);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
yield* settlePendingReplies(pendingReplies, id);
|
|
334
|
+
const freshQueue = yield* Queue.unbounded();
|
|
335
|
+
yield* Ref.set(eventQueueRef, freshQueue);
|
|
336
|
+
yield* SubscriptionRef.set(stateRef, machine.initial);
|
|
337
|
+
yield* Ref.set(stoppedRef, false);
|
|
338
|
+
childrenMap.clear();
|
|
339
|
+
runtimeRef.current = yield* spawnGeneration(machine);
|
|
340
|
+
generation++;
|
|
341
|
+
if (options?.onRestart !== void 0) yield* options.onRestart(generation, generationExit);
|
|
342
|
+
notifyListeners(listeners, machine.initial);
|
|
343
|
+
}
|
|
344
|
+
}));
|
|
345
|
+
else yield* Effect.forkDetach(Deferred.await(runtime.exitDeferred).pipe(Effect.tap((exit) => Deferred.succeed(terminalExitDeferred, exit))));
|
|
346
|
+
return buildActorRefCore(id, machine, stateRef, eventQueueRef, stoppedRef, listeners, Effect.gen(function* () {
|
|
347
|
+
if (supervisorFiber !== void 0) yield* Fiber.interrupt(supervisorFiber);
|
|
348
|
+
const currentRuntime = runtimeRef.current;
|
|
349
|
+
if (currentRuntime !== void 0) yield* currentRuntime.stop;
|
|
350
|
+
yield* Deferred.succeed(terminalExitDeferred, { _tag: "Stopped" });
|
|
247
351
|
if (implicitSystemScope !== void 0) yield* Scope.close(implicitSystemScope, Exit.void);
|
|
248
|
-
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub);
|
|
352
|
+
}).pipe(Effect.withSpan("effect-machine.actor.stop"), Effect.asVoid), system, childrenMap, pendingReplies, transitionsPubSub, terminalExitDeferred);
|
|
249
353
|
});
|
|
250
354
|
/** Fail all pending call/ask Deferreds with ActorStoppedError. Safe to call multiple times. */
|
|
251
355
|
const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
@@ -253,194 +357,6 @@ const settlePendingReplies = (pendingReplies, actorId) => Effect.sync(() => {
|
|
|
253
357
|
for (const deferred of pendingReplies) Effect.runFork(Deferred.fail(deferred, error));
|
|
254
358
|
pendingReplies.clear();
|
|
255
359
|
});
|
|
256
|
-
/**
|
|
257
|
-
* Main event loop for the actor.
|
|
258
|
-
* Includes postpone buffer — events matching postpone rules are buffered
|
|
259
|
-
* and drained after state tag changes (gen_statem semantics).
|
|
260
|
-
*/
|
|
261
|
-
const eventLoop = Effect.fn("effect-machine.actor.eventLoop")(function* (machine, stateRef, eventQueue, stoppedRef, self, listeners, backgroundFibers, stateScopeRef, actorId, inspector, system, pendingReplies, transitionsPubSub) {
|
|
262
|
-
const postponed = [];
|
|
263
|
-
const hasPostponeRules = machine.postponeRules.length > 0;
|
|
264
|
-
const processQueued = Effect.fn("effect-machine.actor.processQueued")(function* (queued) {
|
|
265
|
-
const event = queued.event;
|
|
266
|
-
const currentState = yield* SubscriptionRef.get(stateRef);
|
|
267
|
-
if (hasPostponeRules && shouldPostpone(machine, currentState._tag, event._tag)) {
|
|
268
|
-
postponed.push(queued);
|
|
269
|
-
if (queued._tag === "call") {
|
|
270
|
-
const postponedResult = {
|
|
271
|
-
newState: currentState,
|
|
272
|
-
previousState: currentState,
|
|
273
|
-
transitioned: false,
|
|
274
|
-
lifecycleRan: false,
|
|
275
|
-
isFinal: false,
|
|
276
|
-
hasReply: false,
|
|
277
|
-
reply: void 0,
|
|
278
|
-
postponed: true
|
|
279
|
-
};
|
|
280
|
-
yield* Deferred.succeed(queued.reply, postponedResult);
|
|
281
|
-
}
|
|
282
|
-
return {
|
|
283
|
-
shouldStop: false,
|
|
284
|
-
stateChanged: false
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
const { shouldStop, result } = yield* Effect.withSpan("effect-machine.event.process", { attributes: {
|
|
288
|
-
"effect_machine.actor.id": actorId,
|
|
289
|
-
"effect_machine.state.current": currentState._tag,
|
|
290
|
-
"effect_machine.event.type": event._tag
|
|
291
|
-
} })(processEvent(machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system));
|
|
292
|
-
switch (queued._tag) {
|
|
293
|
-
case "call":
|
|
294
|
-
yield* Deferred.succeed(queued.reply, result);
|
|
295
|
-
break;
|
|
296
|
-
case "ask":
|
|
297
|
-
if (result.hasReply) yield* Deferred.succeed(queued.reply, result.reply);
|
|
298
|
-
else yield* Deferred.fail(queued.reply, new NoReplyError({
|
|
299
|
-
actorId,
|
|
300
|
-
eventTag: event._tag
|
|
301
|
-
}));
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
if (result.transitioned) yield* PubSub.publish(transitionsPubSub, {
|
|
305
|
-
fromState: result.previousState,
|
|
306
|
-
toState: result.newState,
|
|
307
|
-
event
|
|
308
|
-
});
|
|
309
|
-
return {
|
|
310
|
-
shouldStop,
|
|
311
|
-
stateChanged: result.lifecycleRan
|
|
312
|
-
};
|
|
313
|
-
});
|
|
314
|
-
while (true) {
|
|
315
|
-
const { shouldStop, stateChanged } = yield* processQueued(yield* Queue.take(eventQueue));
|
|
316
|
-
if (shouldStop) {
|
|
317
|
-
yield* Ref.set(stoppedRef, true);
|
|
318
|
-
settlePostponedBuffer(postponed, pendingReplies, actorId);
|
|
319
|
-
yield* settlePendingReplies(pendingReplies, actorId);
|
|
320
|
-
yield* Scope.close(stateScopeRef.current, Exit.void);
|
|
321
|
-
yield* Effect.all(backgroundFibers.map(Fiber.interrupt), { concurrency: "unbounded" });
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
let drainTriggered = stateChanged;
|
|
325
|
-
while (drainTriggered && postponed.length > 0) {
|
|
326
|
-
drainTriggered = false;
|
|
327
|
-
const drained = postponed.splice(0);
|
|
328
|
-
for (const entry of drained) {
|
|
329
|
-
const drain = yield* processQueued(entry);
|
|
330
|
-
if (drain.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
|
-
if (drain.stateChanged) drainTriggered = true;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
/**
|
|
344
|
-
* Settle all reply-bearing entries in the postpone buffer on shutdown.
|
|
345
|
-
* Call entries already had their Deferred settled with the postponed result
|
|
346
|
-
* (so their pendingReplies entry is already removed). Ask/send entries
|
|
347
|
-
* with Deferreds are settled via the pendingReplies registry.
|
|
348
|
-
*/
|
|
349
|
-
const settlePostponedBuffer = (postponed, _pendingReplies, _actorId) => {
|
|
350
|
-
postponed.length = 0;
|
|
351
|
-
};
|
|
352
|
-
/**
|
|
353
|
-
* Process a single event, returning true if the actor should stop.
|
|
354
|
-
* Wraps processEventCore with actor-specific concerns (inspection, listeners, state ref).
|
|
355
|
-
*/
|
|
356
|
-
const processEvent = Effect.fn("effect-machine.actor.processEvent")(function* (machine, currentState, event, stateRef, self, listeners, stateScopeRef, actorId, inspector, system) {
|
|
357
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
358
|
-
type: "@machine.event",
|
|
359
|
-
actorId,
|
|
360
|
-
state: currentState,
|
|
361
|
-
event,
|
|
362
|
-
timestamp
|
|
363
|
-
}));
|
|
364
|
-
const result = yield* processEventCore(machine, currentState, event, self, stateScopeRef, system, actorId, inspector === void 0 ? void 0 : {
|
|
365
|
-
onSpawnEffect: (state) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
366
|
-
type: "@machine.effect",
|
|
367
|
-
actorId,
|
|
368
|
-
effectType: "spawn",
|
|
369
|
-
state,
|
|
370
|
-
timestamp
|
|
371
|
-
})),
|
|
372
|
-
onTransition: (from, to, ev) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
373
|
-
type: "@machine.transition",
|
|
374
|
-
actorId,
|
|
375
|
-
fromState: from,
|
|
376
|
-
toState: to,
|
|
377
|
-
event: ev,
|
|
378
|
-
timestamp
|
|
379
|
-
})),
|
|
380
|
-
onError: (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
381
|
-
type: "@machine.error",
|
|
382
|
-
actorId,
|
|
383
|
-
phase: info.phase,
|
|
384
|
-
state: info.state,
|
|
385
|
-
event: info.event,
|
|
386
|
-
error: Cause.pretty(info.cause),
|
|
387
|
-
timestamp
|
|
388
|
-
}))
|
|
389
|
-
});
|
|
390
|
-
if (!result.transitioned) {
|
|
391
|
-
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", false);
|
|
392
|
-
return {
|
|
393
|
-
shouldStop: false,
|
|
394
|
-
result
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
yield* Effect.annotateCurrentSpan("effect_machine.transition.matched", true);
|
|
398
|
-
yield* SubscriptionRef.set(stateRef, result.newState);
|
|
399
|
-
notifyListeners(listeners, result.newState);
|
|
400
|
-
if (result.lifecycleRan) {
|
|
401
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.from", result.previousState._tag);
|
|
402
|
-
yield* Effect.annotateCurrentSpan("effect_machine.state.to", result.newState._tag);
|
|
403
|
-
if (result.isFinal) {
|
|
404
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
405
|
-
type: "@machine.stop",
|
|
406
|
-
actorId,
|
|
407
|
-
finalState: result.newState,
|
|
408
|
-
timestamp
|
|
409
|
-
}));
|
|
410
|
-
return {
|
|
411
|
-
shouldStop: true,
|
|
412
|
-
result
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return {
|
|
417
|
-
shouldStop: false,
|
|
418
|
-
result
|
|
419
|
-
};
|
|
420
|
-
});
|
|
421
|
-
/**
|
|
422
|
-
* Run spawn effects with actor-specific inspection and tracing.
|
|
423
|
-
* Wraps the core runSpawnEffects with inspection events and spans.
|
|
424
|
-
* @internal
|
|
425
|
-
*/
|
|
426
|
-
const runSpawnEffectsWithInspection = Effect.fn("effect-machine.actor.spawnEffects")(function* (machine, state, event, self, stateScope, actorId, inspector, system) {
|
|
427
|
-
yield* emitWithTimestamp(inspector, (timestamp) => ({
|
|
428
|
-
type: "@machine.effect",
|
|
429
|
-
actorId,
|
|
430
|
-
effectType: "spawn",
|
|
431
|
-
state,
|
|
432
|
-
timestamp
|
|
433
|
-
}));
|
|
434
|
-
yield* runSpawnEffects(machine, state, event, self, stateScope, system, actorId, inspector === void 0 ? void 0 : (info) => emitWithTimestamp(inspector, (timestamp) => ({
|
|
435
|
-
type: "@machine.error",
|
|
436
|
-
actorId,
|
|
437
|
-
phase: info.phase,
|
|
438
|
-
state: info.state,
|
|
439
|
-
event: info.event,
|
|
440
|
-
error: Cause.pretty(info.cause),
|
|
441
|
-
timestamp
|
|
442
|
-
})));
|
|
443
|
-
});
|
|
444
360
|
/** Notify all system event listeners (sync). */
|
|
445
361
|
const notifySystemListeners = (listeners, event) => {
|
|
446
362
|
for (const listener of listeners) try {
|
|
@@ -479,7 +395,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
479
395
|
yield* emitSystemEvent({
|
|
480
396
|
_tag: "ActorStopped",
|
|
481
397
|
id,
|
|
482
|
-
actor: actorRef
|
|
398
|
+
actor: actorRef,
|
|
399
|
+
exit: { _tag: "Stopped" }
|
|
483
400
|
});
|
|
484
401
|
MutableHashMap.remove(actorsMap, id);
|
|
485
402
|
}
|
|
@@ -487,11 +404,23 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
487
404
|
}));
|
|
488
405
|
return actor;
|
|
489
406
|
});
|
|
490
|
-
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id,
|
|
407
|
+
const spawnRegular = Effect.fn("effect-machine.actorSystem.spawnRegular")(function* (id, machine, spawnOptions) {
|
|
491
408
|
if (MutableHashMap.has(actorsMap, id)) return yield* new DuplicateActorError({ actorId: id });
|
|
492
|
-
|
|
409
|
+
let actorRef;
|
|
410
|
+
const actor = yield* createActor(id, machine, {
|
|
411
|
+
supervision: spawnOptions?.supervision,
|
|
412
|
+
onRestart: spawnOptions?.supervision !== void 0 ? (generation, exit) => actorRef !== void 0 ? emitSystemEvent({
|
|
413
|
+
_tag: "ActorRestarted",
|
|
414
|
+
id,
|
|
415
|
+
actor: actorRef,
|
|
416
|
+
generation,
|
|
417
|
+
exit
|
|
418
|
+
}) : Effect.void : void 0
|
|
419
|
+
});
|
|
420
|
+
actorRef = actor;
|
|
421
|
+
return yield* registerActor(id, actor);
|
|
493
422
|
});
|
|
494
|
-
const spawn = (id, machine) => withSpawnGate(spawnRegular(id, machine));
|
|
423
|
+
const spawn = (id, machine, options) => withSpawnGate(spawnRegular(id, machine, options));
|
|
495
424
|
const get = Effect.fn("effect-machine.actorSystem.get")(function* (id) {
|
|
496
425
|
return yield* Effect.sync(() => MutableHashMap.get(actorsMap, id));
|
|
497
426
|
});
|
|
@@ -503,7 +432,8 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
503
432
|
yield* emitSystemEvent({
|
|
504
433
|
_tag: "ActorStopped",
|
|
505
434
|
id,
|
|
506
|
-
actor
|
|
435
|
+
actor,
|
|
436
|
+
exit: { _tag: "Stopped" }
|
|
507
437
|
});
|
|
508
438
|
yield* actor.stop;
|
|
509
439
|
return true;
|
|
@@ -529,8 +459,13 @@ const make = Effect.fn("effect-machine.actorSystem.make")(function* () {
|
|
|
529
459
|
});
|
|
530
460
|
});
|
|
531
461
|
/**
|
|
462
|
+
* Create an ActorSystem instance. Must be run in a Scope.
|
|
463
|
+
* @internal — use Default layer for normal usage
|
|
464
|
+
*/
|
|
465
|
+
const makeSystem = make;
|
|
466
|
+
/**
|
|
532
467
|
* Default ActorSystem layer
|
|
533
468
|
*/
|
|
534
469
|
const Default = Layer.effect(ActorSystem, make());
|
|
535
470
|
//#endregion
|
|
536
|
-
export { ActorSystem, Default, buildActorRefCore, createActor, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
471
|
+
export { ActorSystem, Default, buildActorRefCore, createActor, makeSystem, notifyListeners, processEventCore, resolveTransition, runSpawnEffects, settlePendingReplies };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { PersistedEvent, PersistenceAdapter, Snapshot } from "../persistence.js";
|
|
2
|
+
import { Effect, Layer, Ref } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/cluster/adapters/in-memory.d.ts
|
|
5
|
+
interface EntityStore {
|
|
6
|
+
snapshot: Snapshot<unknown> | undefined;
|
|
7
|
+
events: Array<PersistedEvent<unknown>>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create an in-memory persistence adapter.
|
|
11
|
+
*
|
|
12
|
+
* Returns a Layer providing `PersistenceAdapter` and a ref
|
|
13
|
+
* to the backing store for test assertions.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const { layer, storeRef } = yield* makeInMemoryPersistenceAdapter
|
|
18
|
+
* // Use layer to provide PersistenceAdapter
|
|
19
|
+
* // Inspect storeRef for test assertions
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
declare const makeInMemoryPersistenceAdapter: Effect.Effect<{
|
|
23
|
+
adapter: PersistenceAdapter;
|
|
24
|
+
storeRef: Ref.Ref<Map<string, EntityStore>>;
|
|
25
|
+
layer: Layer.Layer<PersistenceAdapter, never, never>;
|
|
26
|
+
}, never, never>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { makeInMemoryPersistenceAdapter };
|